]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #4827 from BookStackApp/query_revamp
authorDan Brown <redacted>
Sun, 11 Feb 2024 15:56:32 +0000 (15:56 +0000)
committerGitHub <redacted>
Sun, 11 Feb 2024 15:56:32 +0000 (15:56 +0000)
Update of entity loading to be more efficient and avoid global addSelects

87 files changed:
app/Activity/ActivityQueries.php
app/Activity/Controllers/CommentController.php
app/Activity/Controllers/FavouriteController.php
app/Api/ApiDocsGenerator.php
app/App/HomeController.php
app/App/Providers/ThemeServiceProvider.php
app/Config/clockwork.php
app/Console/Commands/CopyShelfPermissionsCommand.php
app/Entities/Controllers/BookApiController.php
app/Entities/Controllers/BookController.php
app/Entities/Controllers/BookExportApiController.php
app/Entities/Controllers/BookExportController.php
app/Entities/Controllers/BookSortController.php
app/Entities/Controllers/BookshelfApiController.php
app/Entities/Controllers/BookshelfController.php
app/Entities/Controllers/ChapterApiController.php
app/Entities/Controllers/ChapterController.php
app/Entities/Controllers/ChapterExportApiController.php
app/Entities/Controllers/ChapterExportController.php
app/Entities/Controllers/PageApiController.php
app/Entities/Controllers/PageController.php
app/Entities/Controllers/PageExportApiController.php
app/Entities/Controllers/PageExportController.php
app/Entities/Controllers/PageRevisionController.php
app/Entities/Controllers/PageTemplateController.php
app/Entities/Controllers/RecycleBinController.php
app/Entities/Models/Book.php
app/Entities/Models/BookChild.php
app/Entities/Models/Chapter.php
app/Entities/Models/Page.php
app/Entities/Queries/BookQueries.php [new file with mode: 0644]
app/Entities/Queries/BookshelfQueries.php [new file with mode: 0644]
app/Entities/Queries/ChapterQueries.php [new file with mode: 0644]
app/Entities/Queries/EntityQueries.php [new file with mode: 0644]
app/Entities/Queries/EntityQuery.php [deleted file]
app/Entities/Queries/PageQueries.php [new file with mode: 0644]
app/Entities/Queries/PageRevisionQueries.php [new file with mode: 0644]
app/Entities/Queries/ProvidesEntityQueries.php [new file with mode: 0644]
app/Entities/Queries/QueryPopular.php [moved from app/Entities/Queries/Popular.php with 80% similarity]
app/Entities/Queries/QueryRecentlyViewed.php [new file with mode: 0644]
app/Entities/Queries/QueryTopFavourites.php [moved from app/Entities/Queries/TopFavourites.php with 63% similarity]
app/Entities/Queries/RecentlyViewed.php [deleted file]
app/Entities/Repos/BaseRepo.php
app/Entities/Repos/BookRepo.php
app/Entities/Repos/BookshelfRepo.php
app/Entities/Repos/ChapterRepo.php
app/Entities/Repos/PageRepo.php
app/Entities/Repos/RevisionRepo.php
app/Entities/Tools/BookContents.php
app/Entities/Tools/Cloner.php
app/Entities/Tools/MixedEntityListLoader.php
app/Entities/Tools/PageContent.php
app/Entities/Tools/PageEditorData.php
app/Entities/Tools/ShelfContext.php
app/Entities/Tools/SiblingFetcher.php
app/Entities/Tools/TrashCan.php
app/Permissions/JointPermissionBuilder.php
app/Permissions/PermissionsController.php
app/References/CrossLinkParser.php
app/References/ModelResolvers/BookLinkModelResolver.php
app/References/ModelResolvers/BookshelfLinkModelResolver.php
app/References/ModelResolvers/ChapterLinkModelResolver.php
app/References/ModelResolvers/PageLinkModelResolver.php
app/References/ModelResolvers/PagePermalinkModelResolver.php
app/References/ReferenceController.php
app/References/ReferenceFetcher.php
app/Search/SearchController.php
app/Search/SearchRunner.php
app/Settings/MaintenanceController.php
app/Uploads/AttachmentService.php
app/Uploads/Controllers/AttachmentApiController.php
app/Uploads/Controllers/AttachmentController.php
app/Uploads/Controllers/GalleryImageController.php
app/Uploads/Controllers/ImageGalleryApiController.php
app/Uploads/ImageRepo.php
app/Uploads/ImageService.php
app/Users/Controllers/UserProfileController.php
app/Users/Queries/UserContentCounts.php
app/Users/Queries/UserRecentlyCreatedContent.php
database/migrations/2024_02_04_141358_add_views_updated_index.php [new file with mode: 0644]
resources/views/errors/404.blade.php
tests/Api/BooksApiTest.php
tests/Api/ChaptersApiTest.php
tests/Api/PagesApiTest.php
tests/Api/ShelvesApiTest.php
tests/Entity/BookTest.php
tests/Entity/EntitySearchTest.php

index c69cf7a36e70612c226dd6ddf293cf01d6338fd2..dae0791b1cd85d7ababf89c63ad1070bd7741131 100644 (file)
@@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\MixedEntityListLoader;
 use BookStack\Permissions\PermissionApplicator;
 use BookStack\Users\Models\User;
 use Illuminate\Database\Eloquent\Builder;
@@ -14,11 +15,10 @@ use Illuminate\Database\Eloquent\Relations\Relation;
 
 class ActivityQueries
 {
-    protected PermissionApplicator $permissions;
-
-    public function __construct(PermissionApplicator $permissions)
-    {
-        $this->permissions = $permissions;
+    public function __construct(
+        protected PermissionApplicator $permissions,
+        protected MixedEntityListLoader $listLoader,
+    ) {
     }
 
     /**
@@ -29,11 +29,13 @@ class ActivityQueries
         $activityList = $this->permissions
             ->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
             ->orderBy('created_at', 'desc')
-            ->with(['user', 'entity'])
+            ->with(['user'])
             ->skip($count * $page)
             ->take($count)
             ->get();
 
+        $this->listLoader->loadIntoRelations($activityList->all(), 'entity', false);
+
         return $this->filterSimilar($activityList);
     }
 
index 340524cd069193d36129414921a37c29ee35bb54..52ccc823864e9e6e3221580b080aa0019968eb40 100644 (file)
@@ -3,7 +3,7 @@
 namespace BookStack\Activity\Controllers;
 
 use BookStack\Activity\CommentRepo;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
@@ -11,7 +11,8 @@ use Illuminate\Validation\ValidationException;
 class CommentController extends Controller
 {
     public function __construct(
-        protected CommentRepo $commentRepo
+        protected CommentRepo $commentRepo,
+        protected PageQueries $pageQueries,
     ) {
     }
 
@@ -27,7 +28,7 @@ class CommentController extends Controller
             'parent_id' => ['nullable', 'integer'],
         ]);
 
-        $page = Page::visible()->find($pageId);
+        $page = $this->pageQueries->findVisibleById($pageId);
         if ($page === null) {
             return response('Not found', 404);
         }
index b3aff1cef421e5c572e27024db762f719bad9781..deeb4b0afb45265b635fc16c9ac454236ef1c19d 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Activity\Controllers;
 
-use BookStack\Entities\Queries\TopFavourites;
+use BookStack\Entities\Queries\QueryTopFavourites;
 use BookStack\Entities\Tools\MixedEntityRequestHelper;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
@@ -17,11 +17,11 @@ class FavouriteController extends Controller
     /**
      * Show a listing of all favourite items for the current user.
      */
-    public function index(Request $request)
+    public function index(Request $request, QueryTopFavourites $topFavourites)
     {
         $viewCount = 20;
         $page = intval($request->get('page', 1));
-        $favourites = (new TopFavourites())->run($viewCount + 1, (($page - 1) * $viewCount));
+        $favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount));
 
         $hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
 
index bffc38623bfa75e0dfeea3f51f696e7413116b18..9cae80617e5ae2a44c76003f50a0bd32b6744a61 100644 (file)
@@ -7,7 +7,6 @@ use Exception;
 use Illuminate\Contracts\Container\BindingResolutionException;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Route;
 use Illuminate\Support\Str;
 use Illuminate\Validation\Rules\Password;
index 8188ad0102f2bf0dd738f2d0ab214a37d1d1929e..116f5c8a4bce024a3ba531fdfe5b9241628f9c66 100644 (file)
@@ -3,12 +3,10 @@
 namespace BookStack\App;
 
 use BookStack\Activity\ActivityQueries;
-use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Page;
-use BookStack\Entities\Queries\RecentlyViewed;
-use BookStack\Entities\Queries\TopFavourites;
-use BookStack\Entities\Repos\BookRepo;
-use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Entities\Queries\QueryRecentlyViewed;
+use BookStack\Entities\Queries\QueryTopFavourites;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Http\Controller;
 use BookStack\Uploads\FaviconHandler;
@@ -17,18 +15,25 @@ use Illuminate\Http\Request;
 
 class HomeController extends Controller
 {
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
+
     /**
      * Display the homepage.
      */
-    public function index(Request $request, ActivityQueries $activities)
-    {
+    public function index(
+        Request $request,
+        ActivityQueries $activities,
+        QueryRecentlyViewed $recentlyViewed,
+        QueryTopFavourites $topFavourites,
+    ) {
         $activity = $activities->latest(10);
         $draftPages = [];
 
         if ($this->isSignedIn()) {
-            $draftPages = Page::visible()
-                ->where('draft', '=', true)
-                ->where('created_by', '=', user()->id)
+            $draftPages = $this->queries->pages->currentUserDraftsForList()
                 ->orderBy('updated_at', 'desc')
                 ->with('book')
                 ->take(6)
@@ -37,14 +42,13 @@ class HomeController extends Controller
 
         $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
         $recents = $this->isSignedIn() ?
-            (new RecentlyViewed())->run(12 * $recentFactor, 1)
-            : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
-        $favourites = (new TopFavourites())->run(6);
-        $recentlyUpdatedPages = Page::visible()->with('book')
+            $recentlyViewed->run(12 * $recentFactor, 1)
+            : $this->queries->books->visibleForList()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
+        $favourites = $topFavourites->run(6);
+        $recentlyUpdatedPages = $this->queries->pages->visibleForList()
             ->where('draft', false)
             ->orderBy('updated_at', 'desc')
             ->take($favourites->count() > 0 ? 5 : 10)
-            ->select(Page::$listAttributes)
             ->get();
 
         $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@@ -78,14 +82,18 @@ class HomeController extends Controller
         }
 
         if ($homepageOption === 'bookshelves') {
-            $shelves = app()->make(BookshelfRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
+            $shelves = $this->queries->shelves->visibleForListWithCover()
+                ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
+                ->paginate(18);
             $data = array_merge($commonData, ['shelves' => $shelves]);
 
             return view('home.shelves', $data);
         }
 
         if ($homepageOption === 'books') {
-            $books = app()->make(BookRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
+            $books = $this->queries->books->visibleForListWithCover()
+                ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
+                ->paginate(18);
             $data = array_merge($commonData, ['books' => $books]);
 
             return view('home.books', $data);
@@ -95,7 +103,7 @@ class HomeController extends Controller
             $homepageSetting = setting('app-homepage', '0:');
             $id = intval(explode(':', $homepageSetting)[0]);
             /** @var Page $customHomepage */
-            $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
+            $customHomepage = $this->queries->pages->start()->where('draft', '=', false)->findOrFail($id);
             $pageContent = new PageContent($customHomepage);
             $customHomepage->html = $pageContent->render(false);
 
index c15b43a6b4cdfe11002450717da9ef3b64a25fa5..4c657d9123208dca5f7aaae592e3f1cfc152fb32 100644 (file)
@@ -4,7 +4,6 @@ namespace BookStack\App\Providers;
 
 use BookStack\Theming\ThemeEvents;
 use BookStack\Theming\ThemeService;
-use Illuminate\Support\Facades\Route;
 use Illuminate\Support\ServiceProvider;
 
 class ThemeServiceProvider extends ServiceProvider
index 394af845172af8d5356f14b97a8c75d0cb856a1b..bd59eaf71d858583f61d0346aeb4e5e657211725 100644 (file)
@@ -173,6 +173,8 @@ return [
 
         // List of URIs that should not be collected
         'except' => [
+            '/uploads/images/.*', // BookStack image requests
+
             '/horizon/.*', // Laravel Horizon requests
             '/telescope/.*', // Laravel Telescope requests
             '/_debugbar/.*', // Laravel DebugBar requests
index fc11484bd4dfa3da89650e49a159664a05c372d8..c5e2d504e759bea142749aba89c6c8a53a378853 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Console\Commands;
 
-use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
 use BookStack\Entities\Tools\PermissionsUpdater;
 use Illuminate\Console\Command;
 
@@ -28,7 +28,7 @@ class CopyShelfPermissionsCommand extends Command
     /**
      * Execute the console command.
      */
-    public function handle(PermissionsUpdater $permissionsUpdater): int
+    public function handle(PermissionsUpdater $permissionsUpdater, BookshelfQueries $queries): int
     {
         $shelfSlug = $this->option('slug');
         $cascadeAll = $this->option('all');
@@ -51,11 +51,11 @@ class CopyShelfPermissionsCommand extends Command
                 return 0;
             }
 
-            $shelves = Bookshelf::query()->get(['id']);
+            $shelves = $queries->start()->get(['id']);
         }
 
         if ($shelfSlug) {
-            $shelves = Bookshelf::query()->where('slug', '=', $shelfSlug)->get(['id']);
+            $shelves = $queries->start()->where('slug', '=', $shelfSlug)->get(['id']);
             if ($shelves->count() === 0) {
                 $this->info('No shelves found with the given slug.');
             }
index aa21aea472a8d044d718cb1d04d0e4745e0a7115..15e67a0f7231256869f05248e090b33973e1cb74 100644 (file)
@@ -6,6 +6,7 @@ use BookStack\Api\ApiEntityListFormatter;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Queries\BookQueries;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Http\ApiController;
@@ -15,7 +16,8 @@ use Illuminate\Validation\ValidationException;
 class BookApiController extends ApiController
 {
     public function __construct(
-        protected BookRepo $bookRepo
+        protected BookRepo $bookRepo,
+        protected BookQueries $queries,
     ) {
     }
 
@@ -24,7 +26,9 @@ class BookApiController extends ApiController
      */
     public function list()
     {
-        $books = Book::visible();
+        $books = $this->queries
+            ->visibleForList()
+            ->addSelect(['created_by', 'updated_by']);
 
         return $this->apiListingResponse($books, [
             'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
@@ -56,7 +60,7 @@ class BookApiController extends ApiController
      */
     public function read(string $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail(intval($id));
         $book = $this->forJsonDisplay($book);
         $book->load(['createdBy', 'updatedBy', 'ownedBy']);
 
@@ -83,7 +87,7 @@ class BookApiController extends ApiController
      */
     public function update(Request $request, string $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail(intval($id));
         $this->checkOwnablePermission('book-update', $book);
 
         $requestData = $this->validate($request, $this->rules()['update']);
@@ -100,7 +104,7 @@ class BookApiController extends ApiController
      */
     public function delete(string $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail(intval($id));
         $this->checkOwnablePermission('book-delete', $book);
 
         $this->bookRepo->destroy($book);
index 412feca2fe5b24fee76cc1068ca3583dbcccae04..a1c586f47e7ce9fca429fca433f1e41de07872f2 100644 (file)
@@ -6,7 +6,8 @@ use BookStack\Activity\ActivityQueries;
 use BookStack\Activity\ActivityType;
 use BookStack\Activity\Models\View;
 use BookStack\Activity\Tools\UserEntityWatchOptions;
-use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookQueries;
+use BookStack\Entities\Queries\BookshelfQueries;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\Cloner;
@@ -27,7 +28,9 @@ class BookController extends Controller
     public function __construct(
         protected ShelfContext $shelfContext,
         protected BookRepo $bookRepo,
-        protected ReferenceFetcher $referenceFetcher
+        protected BookQueries $queries,
+        protected BookshelfQueries $shelfQueries,
+        protected ReferenceFetcher $referenceFetcher,
     ) {
     }
 
@@ -43,10 +46,12 @@ class BookController extends Controller
             'updated_at' => trans('common.sort_updated_at'),
         ]);
 
-        $books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
-        $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
-        $popular = $this->bookRepo->getPopular(4);
-        $new = $this->bookRepo->getRecentlyCreated(4);
+        $books = $this->queries->visibleForListWithCover()
+            ->orderBy($listOptions->getSort(), $listOptions->getOrder())
+            ->paginate(18);
+        $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false;
+        $popular = $this->queries->popularForList()->take(4)->get();
+        $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get();
 
         $this->shelfContext->clearShelfContext();
 
@@ -71,7 +76,7 @@ class BookController extends Controller
 
         $bookshelf = null;
         if ($shelfSlug !== null) {
-            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
+            $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
         }
 
@@ -101,7 +106,7 @@ class BookController extends Controller
 
         $bookshelf = null;
         if ($shelfSlug !== null) {
-            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
+            $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
         }
 
@@ -120,7 +125,7 @@ class BookController extends Controller
      */
     public function show(Request $request, ActivityQueries $activities, string $slug)
     {
-        $book = $this->bookRepo->getBySlug($slug);
+        $book = $this->queries->findVisibleBySlugOrFail($slug);
         $bookChildren = (new BookContents($book))->getTree(true);
         $bookParentShelves = $book->shelves()->scopes('visible')->get();
 
@@ -147,7 +152,7 @@ class BookController extends Controller
      */
     public function edit(string $slug)
     {
-        $book = $this->bookRepo->getBySlug($slug);
+        $book = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('book-update', $book);
         $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
 
@@ -163,7 +168,7 @@ class BookController extends Controller
      */
     public function update(Request $request, string $slug)
     {
-        $book = $this->bookRepo->getBySlug($slug);
+        $book = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('book-update', $book);
 
         $validated = $this->validate($request, [
@@ -190,7 +195,7 @@ class BookController extends Controller
      */
     public function showDelete(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
         $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
 
@@ -204,7 +209,7 @@ class BookController extends Controller
      */
     public function destroy(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
 
         $this->bookRepo->destroy($book);
@@ -219,7 +224,7 @@ class BookController extends Controller
      */
     public function showCopy(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-view', $book);
 
         session()->flashInput(['name' => $book->name]);
@@ -236,7 +241,7 @@ class BookController extends Controller
      */
     public function copy(Request $request, Cloner $cloner, string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-view', $book);
         $this->checkPermission('book-create-all');
 
@@ -252,7 +257,7 @@ class BookController extends Controller
      */
     public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-update', $book);
         $this->checkOwnablePermission('book-delete', $book);
         $this->checkPermission('bookshelf-create-all');
index 5b6826c193dcb9f40b8abca33d2d4ab439c1e848..1161ddb8886964423f088630424317225914eb74 100644 (file)
@@ -2,18 +2,17 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\BookQueries;
 use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Http\ApiController;
 use Throwable;
 
 class BookExportApiController extends ApiController
 {
-    protected $exportFormatter;
-
-    public function __construct(ExportFormatter $exportFormatter)
-    {
-        $this->exportFormatter = $exportFormatter;
+    public function __construct(
+        protected ExportFormatter $exportFormatter,
+        protected BookQueries $queries,
+    ) {
         $this->middleware('can:content-export');
     }
 
@@ -24,7 +23,7 @@ class BookExportApiController extends ApiController
      */
     public function exportPdf(int $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail($id);
         $pdfContent = $this->exportFormatter->bookToPdf($book);
 
         return $this->download()->directly($pdfContent, $book->slug . '.pdf');
@@ -37,7 +36,7 @@ class BookExportApiController extends ApiController
      */
     public function exportHtml(int $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail($id);
         $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
 
         return $this->download()->directly($htmlContent, $book->slug . '.html');
@@ -48,7 +47,7 @@ class BookExportApiController extends ApiController
      */
     public function exportPlainText(int $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail($id);
         $textContent = $this->exportFormatter->bookToPlainText($book);
 
         return $this->download()->directly($textContent, $book->slug . '.txt');
@@ -59,7 +58,7 @@ class BookExportApiController extends ApiController
      */
     public function exportMarkdown(int $id)
     {
-        $book = Book::visible()->findOrFail($id);
+        $book = $this->queries->findVisibleByIdOrFail($id);
         $markdown = $this->exportFormatter->bookToMarkdown($book);
 
         return $this->download()->directly($markdown, $book->slug . '.md');
index 1a6b20db966e085455a84727a9af61e6f261aad3..5c1a964c1e1037c2d60bd789cb5a049acb71b1c2 100644 (file)
@@ -2,23 +2,17 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Queries\BookQueries;
 use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Http\Controller;
 use Throwable;
 
 class BookExportController extends Controller
 {
-    protected $bookRepo;
-    protected $exportFormatter;
-
-    /**
-     * BookExportController constructor.
-     */
-    public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
-    {
-        $this->bookRepo = $bookRepo;
-        $this->exportFormatter = $exportFormatter;
+    public function __construct(
+        protected BookQueries $queries,
+        protected ExportFormatter $exportFormatter,
+    ) {
         $this->middleware('can:content-export');
     }
 
@@ -29,7 +23,7 @@ class BookExportController extends Controller
      */
     public function pdf(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $pdfContent = $this->exportFormatter->bookToPdf($book);
 
         return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
@@ -42,7 +36,7 @@ class BookExportController extends Controller
      */
     public function html(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
 
         return $this->download()->directly($htmlContent, $bookSlug . '.html');
@@ -53,7 +47,7 @@ class BookExportController extends Controller
      */
     public function plainText(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $textContent = $this->exportFormatter->bookToPlainText($book);
 
         return $this->download()->directly($textContent, $bookSlug . '.txt');
@@ -64,7 +58,7 @@ class BookExportController extends Controller
      */
     public function markdown(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $textContent = $this->exportFormatter->bookToMarkdown($book);
 
         return $this->download()->directly($textContent, $bookSlug . '.md');
index f2310e2055dd24f76301eec1abd4ac0859c2a483..5aefc583279dbdf7f97cb42122f1af52cf6489a5 100644 (file)
@@ -3,7 +3,7 @@
 namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\ActivityType;
-use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Queries\BookQueries;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\BookSortMap;
 use BookStack\Facades\Activity;
@@ -12,11 +12,9 @@ use Illuminate\Http\Request;
 
 class BookSortController extends Controller
 {
-    protected $bookRepo;
-
-    public function __construct(BookRepo $bookRepo)
-    {
-        $this->bookRepo = $bookRepo;
+    public function __construct(
+        protected BookQueries $queries,
+    ) {
     }
 
     /**
@@ -24,7 +22,7 @@ class BookSortController extends Controller
      */
     public function show(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-update', $book);
 
         $bookChildren = (new BookContents($book))->getTree(false);
@@ -40,7 +38,7 @@ class BookSortController extends Controller
      */
     public function showItem(string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $bookChildren = (new BookContents($book))->getTree();
 
         return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
@@ -51,7 +49,7 @@ class BookSortController extends Controller
      */
     public function update(Request $request, string $bookSlug)
     {
-        $book = $this->bookRepo->getBySlug($bookSlug);
+        $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('book-update', $book);
 
         // Return if no map sent
index a12dc90ac62cf654d34521fe8579ee38072d031b..a665bcb6bab7d314c35939f1c7e6aa3e4e902c28 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Entities\Controllers;
 
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Http\ApiController;
 use Exception;
@@ -13,7 +14,8 @@ use Illuminate\Validation\ValidationException;
 class BookshelfApiController extends ApiController
 {
     public function __construct(
-        protected BookshelfRepo $bookshelfRepo
+        protected BookshelfRepo $bookshelfRepo,
+        protected BookshelfQueries $queries,
     ) {
     }
 
@@ -22,7 +24,9 @@ class BookshelfApiController extends ApiController
      */
     public function list()
     {
-        $shelves = Bookshelf::visible();
+        $shelves = $this->queries
+            ->visibleForList()
+            ->addSelect(['created_by', 'updated_by']);
 
         return $this->apiListingResponse($shelves, [
             'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
@@ -54,7 +58,7 @@ class BookshelfApiController extends ApiController
      */
     public function read(string $id)
     {
-        $shelf = Bookshelf::visible()->findOrFail($id);
+        $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
         $shelf = $this->forJsonDisplay($shelf);
         $shelf->load([
             'createdBy', 'updatedBy', 'ownedBy',
@@ -78,7 +82,7 @@ class BookshelfApiController extends ApiController
      */
     public function update(Request $request, string $id)
     {
-        $shelf = Bookshelf::visible()->findOrFail($id);
+        $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
         $this->checkOwnablePermission('bookshelf-update', $shelf);
 
         $requestData = $this->validate($request, $this->rules()['update']);
@@ -97,7 +101,7 @@ class BookshelfApiController extends ApiController
      */
     public function delete(string $id)
     {
-        $shelf = Bookshelf::visible()->findOrFail($id);
+        $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
         $this->bookshelfRepo->destroy($shelf);
index 2f5461cdb0bbdeaf3eb0a2e640e80fccf4f86dc5..6cedd23e7df244d35e74591d04297a95a761cff8 100644 (file)
@@ -4,7 +4,8 @@ namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\ActivityQueries;
 use BookStack\Activity\Models\View;
-use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\BookQueries;
+use BookStack\Entities\Queries\BookshelfQueries;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Entities\Tools\ShelfContext;
 use BookStack\Exceptions\ImageUploadException;
@@ -20,8 +21,10 @@ class BookshelfController extends Controller
 {
     public function __construct(
         protected BookshelfRepo $shelfRepo,
+        protected BookshelfQueries $queries,
+        protected BookQueries $bookQueries,
         protected ShelfContext $shelfContext,
-        protected ReferenceFetcher $referenceFetcher
+        protected ReferenceFetcher $referenceFetcher,
     ) {
     }
 
@@ -37,10 +40,15 @@ class BookshelfController extends Controller
             'updated_at' => trans('common.sort_updated_at'),
         ]);
 
-        $shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
-        $recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
-        $popular = $this->shelfRepo->getPopular(4);
-        $new = $this->shelfRepo->getRecentlyCreated(4);
+        $shelves = $this->queries->visibleForListWithCover()
+            ->orderBy($listOptions->getSort(), $listOptions->getOrder())
+            ->paginate(18);
+        $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false;
+        $popular = $this->queries->popularForList()->get();
+        $new = $this->queries->visibleForList()
+            ->orderBy('created_at', 'desc')
+            ->take(4)
+            ->get();
 
         $this->shelfContext->clearShelfContext();
         $this->setPageTitle(trans('entities.shelves'));
@@ -61,7 +69,7 @@ class BookshelfController extends Controller
     public function create()
     {
         $this->checkPermission('bookshelf-create-all');
-        $books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
+        $books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
         $this->setPageTitle(trans('entities.shelves_create'));
 
         return view('shelves.create', ['books' => $books]);
@@ -96,7 +104,7 @@ class BookshelfController extends Controller
      */
     public function show(Request $request, ActivityQueries $activities, string $slug)
     {
-        $shelf = $this->shelfRepo->getBySlug($slug);
+        $shelf = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('bookshelf-view', $shelf);
 
         $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
@@ -134,11 +142,14 @@ class BookshelfController extends Controller
      */
     public function edit(string $slug)
     {
-        $shelf = $this->shelfRepo->getBySlug($slug);
+        $shelf = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
 
         $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
-        $books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
+        $books = $this->bookQueries->visibleForList()
+            ->whereNotIn('id', $shelfBookIds)
+            ->orderBy('name')
+            ->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
 
         $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
 
@@ -157,7 +168,7 @@ class BookshelfController extends Controller
      */
     public function update(Request $request, string $slug)
     {
-        $shelf = $this->shelfRepo->getBySlug($slug);
+        $shelf = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
         $validated = $this->validate($request, [
             'name'             => ['required', 'string', 'max:255'],
@@ -183,7 +194,7 @@ class BookshelfController extends Controller
      */
     public function showDelete(string $slug)
     {
-        $shelf = $this->shelfRepo->getBySlug($slug);
+        $shelf = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
         $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
@@ -198,7 +209,7 @@ class BookshelfController extends Controller
      */
     public function destroy(string $slug)
     {
-        $shelf = $this->shelfRepo->getBySlug($slug);
+        $shelf = $this->queries->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
         $this->shelfRepo->destroy($shelf);
index 3fbe852220579d2b0ad36b8b7f3237d5338af658..430654330f36b97d5ec16dbaf22c12a56b337738 100644 (file)
@@ -2,8 +2,9 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\ChapterQueries;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Http\ApiController;
@@ -35,7 +36,9 @@ class ChapterApiController extends ApiController
     ];
 
     public function __construct(
-        protected ChapterRepo $chapterRepo
+        protected ChapterRepo $chapterRepo,
+        protected ChapterQueries $queries,
+        protected EntityQueries $entityQueries,
     ) {
     }
 
@@ -44,7 +47,8 @@ class ChapterApiController extends ApiController
      */
     public function list()
     {
-        $chapters = Chapter::visible();
+        $chapters = $this->queries->visibleForList()
+            ->addSelect(['created_by', 'updated_by']);
 
         return $this->apiListingResponse($chapters, [
             'id', 'book_id', 'name', 'slug', 'description', 'priority',
@@ -60,7 +64,7 @@ class ChapterApiController extends ApiController
         $requestData = $this->validate($request, $this->rules['create']);
 
         $bookId = $request->get('book_id');
-        $book = Book::visible()->findOrFail($bookId);
+        $book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
         $this->checkOwnablePermission('chapter-create', $book);
 
         $chapter = $this->chapterRepo->create($requestData, $book);
@@ -73,15 +77,17 @@ class ChapterApiController extends ApiController
      */
     public function read(string $id)
     {
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
         $chapter = $this->forJsonDisplay($chapter);
 
-        $chapter->load([
-            'createdBy', 'updatedBy', 'ownedBy',
-            'pages' => function (HasMany $query) {
-                $query->scopes('visible')->get(['id', 'name', 'slug']);
-            }
-        ]);
+        $chapter->load(['createdBy', 'updatedBy', 'ownedBy']);
+
+        // Note: More fields than usual here, for backwards compatibility,
+        // due to previously accidentally including more fields that desired.
+        $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)
+            ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor'])
+            ->get();
+        $chapter->setRelation('pages', $pages);
 
         return response()->json($chapter);
     }
@@ -94,7 +100,7 @@ class ChapterApiController extends ApiController
     public function update(Request $request, string $id)
     {
         $requestData = $this->validate($request, $this->rules()['update']);
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
@@ -122,7 +128,7 @@ class ChapterApiController extends ApiController
      */
     public function delete(string $id)
     {
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $this->chapterRepo->destroy($chapter);
index 00616888a701c7aea91cc8f9f8517d9a3e1426c7..4274589e26055c5d1e378156f7d7b0ccd76f87bc 100644 (file)
@@ -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->findVisibleBySlugOrFail($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->findVisibleBySlugOrFail($bookSlug);
         $this->checkOwnablePermission('chapter-create', $book);
 
         $chapter = $this->chapterRepo->create($validated, $book);
@@ -68,11 +75,12 @@ class ChapterController extends Controller
      */
     public function show(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
 
         $sidebarTree = (new BookContents($chapter->book))->getTree();
-        $pages = $chapter->getVisiblePages();
+        $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
+
         $nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
         View::incrementFor($chapter);
 
@@ -96,7 +104,7 @@ class ChapterController extends Controller
      */
     public function edit(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
@@ -118,7 +126,7 @@ class ChapterController extends Controller
             'default_template_id' => ['nullable', 'integer'],
         ]);
 
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
         $this->chapterRepo->update($chapter, $validated);
@@ -133,7 +141,7 @@ class ChapterController extends Controller
      */
     public function showDelete(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
@@ -149,7 +157,7 @@ class ChapterController extends Controller
      */
     public function destroy(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $this->chapterRepo->destroy($chapter);
@@ -164,7 +172,7 @@ class ChapterController extends Controller
      */
     public function showMove(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
@@ -182,7 +190,7 @@ class ChapterController extends Controller
      */
     public function move(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
@@ -211,7 +219,7 @@ class ChapterController extends Controller
      */
     public function showCopy(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
 
         session()->flashInput(['name' => $chapter->name]);
@@ -230,13 +238,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->findVisibleBySlugsOrFail($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 +264,7 @@ class ChapterController extends Controller
      */
     public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
         $this->checkPermission('book-create-all');
index d1523e665c920c6874367bcce40de2c086c4217a..ceb2522b2118212657a9a1bbab666fe42c28581c 100644 (file)
@@ -2,21 +2,17 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\ChapterQueries;
 use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Http\ApiController;
 use Throwable;
 
 class ChapterExportApiController extends ApiController
 {
-    protected $exportFormatter;
-
-    /**
-     * ChapterExportController constructor.
-     */
-    public function __construct(ExportFormatter $exportFormatter)
-    {
-        $this->exportFormatter = $exportFormatter;
+    public function __construct(
+        protected ExportFormatter $exportFormatter,
+        protected ChapterQueries $queries,
+    ) {
         $this->middleware('can:content-export');
     }
 
@@ -27,7 +23,7 @@ class ChapterExportApiController extends ApiController
      */
     public function exportPdf(int $id)
     {
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail($id);
         $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
 
         return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
@@ -40,7 +36,7 @@ class ChapterExportApiController extends ApiController
      */
     public function exportHtml(int $id)
     {
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail($id);
         $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
 
         return $this->download()->directly($htmlContent, $chapter->slug . '.html');
@@ -51,7 +47,7 @@ class ChapterExportApiController extends ApiController
      */
     public function exportPlainText(int $id)
     {
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail($id);
         $textContent = $this->exportFormatter->chapterToPlainText($chapter);
 
         return $this->download()->directly($textContent, $chapter->slug . '.txt');
@@ -62,7 +58,7 @@ class ChapterExportApiController extends ApiController
      */
     public function exportMarkdown(int $id)
     {
-        $chapter = Chapter::visible()->findOrFail($id);
+        $chapter = $this->queries->findVisibleByIdOrFail($id);
         $markdown = $this->exportFormatter->chapterToMarkdown($chapter);
 
         return $this->download()->directly($markdown, $chapter->slug . '.md');
index b67ec9b373b0d29adaf356471e6ab38e31b32a59..ead601ab46b432ca541c74df4d77a15f7cd8aeda 100644 (file)
@@ -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->findVisibleBySlugsOrFail($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->findVisibleBySlugsOrFail($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->findVisibleBySlugsOrFail($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->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
 
         return $this->download()->directly($chapterText, $chapterSlug . '.md');
index d2947f1bb7227c560214fba3e472c0cb2ef0b5d1..40598e2098375afc564f35a371eb22048ef50925 100644 (file)
@@ -2,9 +2,8 @@
 
 namespace BookStack\Entities\Controllers;
 
-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\Exceptions\PermissionsException;
 use BookStack\Http\ApiController;
@@ -35,7 +34,9 @@ class PageApiController extends ApiController
     ];
 
     public function __construct(
-        protected PageRepo $pageRepo
+        protected PageRepo $pageRepo,
+        protected PageQueries $queries,
+        protected EntityQueries $entityQueries,
     ) {
     }
 
@@ -44,7 +45,8 @@ class PageApiController extends ApiController
      */
     public function list()
     {
-        $pages = Page::visible();
+        $pages = $this->queries->visibleForList()
+            ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
 
         return $this->apiListingResponse($pages, [
             'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
@@ -70,9 +72,9 @@ class PageApiController extends ApiController
         $this->validate($request, $this->rules['create']);
 
         if ($request->has('chapter_id')) {
-            $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+            $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
         } else {
-            $parent = Book::visible()->findOrFail($request->get('book_id'));
+            $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
         }
         $this->checkOwnablePermission('page-create', $parent);
 
@@ -97,7 +99,7 @@ class PageApiController extends ApiController
      */
     public function read(string $id)
     {
-        $page = $this->pageRepo->getById($id, []);
+        $page = $this->queries->findVisibleByIdOrFail($id);
 
         return response()->json($page->forJsonDisplay());
     }
@@ -113,14 +115,14 @@ class PageApiController extends ApiController
     {
         $requestData = $this->validate($request, $this->rules['update']);
 
-        $page = $this->pageRepo->getById($id, []);
+        $page = $this->queries->findVisibleByIdOrFail($id);
         $this->checkOwnablePermission('page-update', $page);
 
         $parent = null;
         if ($request->has('chapter_id')) {
-            $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+            $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
         } elseif ($request->has('book_id')) {
-            $parent = Book::visible()->findOrFail($request->get('book_id'));
+            $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
         }
 
         if ($parent && !$parent->matches($page->getParent())) {
@@ -148,7 +150,7 @@ class PageApiController extends ApiController
      */
     public function delete(string $id)
     {
-        $page = $this->pageRepo->getById($id, []);
+        $page = $this->queries->findVisibleByIdOrFail($id);
         $this->checkOwnablePermission('page-delete', $page);
 
         $this->pageRepo->destroy($page);
index eaad3c0b79de3303d1544b45d018cbef42fd75b1..eab53bb2510c361a0b849eee542f2eb8214326bc 100644 (file)
@@ -7,7 +7,8 @@ use BookStack\Activity\Tools\CommentTree;
 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 +30,8 @@ class PageController extends Controller
 {
     public function __construct(
         protected PageRepo $pageRepo,
+        protected PageQueries $queries,
+        protected EntityQueries $entityQueries,
         protected ReferenceFetcher $referenceFetcher
     ) {
     }
@@ -40,7 +43,12 @@ class PageController extends Controller
      */
     public function create(string $bookSlug, string $chapterSlug = null)
     {
-        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
+        if ($chapterSlug) {
+            $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
+        } else {
+            $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
+        }
+
         $this->checkOwnablePermission('page-create', $parent);
 
         // Redirect to draft edit screen if signed in
@@ -67,7 +75,12 @@ class PageController extends Controller
             'name' => ['required', 'string', 'max:255'],
         ]);
 
-        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
+        if ($chapterSlug) {
+            $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
+        } else {
+            $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
+        }
+
         $this->checkOwnablePermission('page-create', $parent);
 
         $page = $this->pageRepo->getNewDraftPage($parent);
@@ -85,10 +98,10 @@ class PageController extends Controller
      */
     public function editDraft(Request $request, string $bookSlug, int $pageId)
     {
-        $draft = $this->pageRepo->getById($pageId);
+        $draft = $this->queries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-create', $draft->getParent());
 
-        $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
+        $editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
         $this->setPageTitle(trans('entities.pages_edit_draft'));
 
         return view('pages.edit', $editorData->getViewData());
@@ -105,7 +118,7 @@ class PageController extends Controller
         $this->validate($request, [
             'name' => ['required', 'string', 'max:255'],
         ]);
-        $draftPage = $this->pageRepo->getById($pageId);
+        $draftPage = $this->queries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-create', $draftPage->getParent());
 
         $page = $this->pageRepo->publishDraft($draftPage, $request->all());
@@ -122,11 +135,12 @@ class PageController extends Controller
     public function show(string $bookSlug, string $pageSlug)
     {
         try {
-            $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+            $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         } catch (NotFoundException $e) {
-            $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
+            $revision = $this->entityQueries->revisions->findLatestVersionBySlugs($bookSlug, $pageSlug);
+            $page = $revision->page ?? null;
 
-            if ($page === null) {
+            if (is_null($page)) {
                 throw $e;
             }
 
@@ -167,7 +181,7 @@ class PageController extends Controller
      */
     public function getPageAjax(int $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->queries->findVisibleByIdOrFail($pageId);
         $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
         $page->makeHidden(['book']);
 
@@ -181,10 +195,10 @@ class PageController extends Controller
      */
     public function edit(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
 
-        $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
+        $editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
         if ($editorData->getWarnings()) {
             $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
         }
@@ -205,7 +219,7 @@ class PageController extends Controller
         $this->validate($request, [
             'name' => ['required', 'string', 'max:255'],
         ]);
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
 
         $this->pageRepo->update($page, $request->all());
@@ -220,7 +234,7 @@ class PageController extends Controller
      */
     public function saveDraft(Request $request, int $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->queries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
         if (!$this->isSignedIn()) {
@@ -245,7 +259,7 @@ class PageController extends Controller
      */
     public function redirectFromLink(int $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->queries->findVisibleByIdOrFail($pageId);
 
         return redirect($page->getUrl());
     }
@@ -257,12 +271,12 @@ class PageController extends Controller
      */
     public function showDelete(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
         $usedAsTemplate =
-            Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
-            Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
+            $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
+            $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
 
         return view('pages.delete', [
             'book'    => $page->book,
@@ -279,12 +293,12 @@ class PageController extends Controller
      */
     public function showDeleteDraft(string $bookSlug, int $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->queries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-update', $page);
         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
         $usedAsTemplate =
-            Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
-            Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
+            $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
+            $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
 
         return view('pages.delete', [
             'book'    => $page->book,
@@ -302,7 +316,7 @@ class PageController extends Controller
      */
     public function destroy(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
         $parent = $page->getParent();
 
@@ -319,7 +333,7 @@ class PageController extends Controller
      */
     public function destroyDraft(string $bookSlug, int $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->queries->findVisibleByIdOrFail($pageId);
         $book = $page->book;
         $chapter = $page->chapter;
         $this->checkOwnablePermission('page-update', $page);
@@ -344,7 +358,9 @@ class PageController extends Controller
             $query->scopes('visible');
         };
 
-        $pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
+        $pages = $this->queries->visibleForList()
+            ->addSelect('updated_by')
+            ->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
             ->orderBy('updated_at', 'desc')
             ->paginate(20)
             ->setPath(url('/pages/recently-updated'));
@@ -366,7 +382,7 @@ class PageController extends Controller
      */
     public function showMove(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('page-delete', $page);
 
@@ -384,7 +400,7 @@ class PageController extends Controller
      */
     public function move(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('page-delete', $page);
 
@@ -413,7 +429,7 @@ class PageController extends Controller
      */
     public function showCopy(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-view', $page);
         session()->flashInput(['name' => $page->name]);
 
@@ -431,13 +447,13 @@ class PageController extends Controller
      */
     public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $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'));
index d936a0de2b49f89606e01d14566e9668d3200fb9..693760bc8e727ddb6ce13a8c004f3a808f816670 100644 (file)
@@ -2,18 +2,17 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Http\ApiController;
 use Throwable;
 
 class PageExportApiController extends ApiController
 {
-    protected $exportFormatter;
-
-    public function __construct(ExportFormatter $exportFormatter)
-    {
-        $this->exportFormatter = $exportFormatter;
+    public function __construct(
+        protected ExportFormatter $exportFormatter,
+        protected PageQueries $queries,
+    ) {
         $this->middleware('can:content-export');
     }
 
@@ -24,7 +23,7 @@ class PageExportApiController extends ApiController
      */
     public function exportPdf(int $id)
     {
-        $page = Page::visible()->findOrFail($id);
+        $page = $this->queries->findVisibleByIdOrFail($id);
         $pdfContent = $this->exportFormatter->pageToPdf($page);
 
         return $this->download()->directly($pdfContent, $page->slug . '.pdf');
@@ -37,7 +36,7 @@ class PageExportApiController extends ApiController
      */
     public function exportHtml(int $id)
     {
-        $page = Page::visible()->findOrFail($id);
+        $page = $this->queries->findVisibleByIdOrFail($id);
         $htmlContent = $this->exportFormatter->pageToContainedHtml($page);
 
         return $this->download()->directly($htmlContent, $page->slug . '.html');
@@ -48,7 +47,7 @@ class PageExportApiController extends ApiController
      */
     public function exportPlainText(int $id)
     {
-        $page = Page::visible()->findOrFail($id);
+        $page = $this->queries->findVisibleByIdOrFail($id);
         $textContent = $this->exportFormatter->pageToPlainText($page);
 
         return $this->download()->directly($textContent, $page->slug . '.txt');
@@ -59,7 +58,7 @@ class PageExportApiController extends ApiController
      */
     public function exportMarkdown(int $id)
     {
-        $page = Page::visible()->findOrFail($id);
+        $page = $this->queries->findVisibleByIdOrFail($id);
         $markdown = $this->exportFormatter->pageToMarkdown($page);
 
         return $this->download()->directly($markdown, $page->slug . '.md');
index 31862c8acc08ed1d222b8c2fe6df52d8e8686c65..be97f1930bdd28e61f5f14d98647b336cc1439cf 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Entities\Controllers;
 
-use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Tools\ExportFormatter;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Exceptions\NotFoundException;
@@ -11,16 +11,10 @@ use Throwable;
 
 class PageExportController extends Controller
 {
-    protected $pageRepo;
-    protected $exportFormatter;
-
-    /**
-     * PageExportController constructor.
-     */
-    public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter)
-    {
-        $this->pageRepo = $pageRepo;
-        $this->exportFormatter = $exportFormatter;
+    public function __construct(
+        protected PageQueries $queries,
+        protected ExportFormatter $exportFormatter,
+    ) {
         $this->middleware('can:content-export');
     }
 
@@ -33,7 +27,7 @@ class PageExportController extends Controller
      */
     public function pdf(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $page->html = (new PageContent($page))->render();
         $pdfContent = $this->exportFormatter->pageToPdf($page);
 
@@ -48,7 +42,7 @@ class PageExportController extends Controller
      */
     public function html(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $page->html = (new PageContent($page))->render();
         $containedHtml = $this->exportFormatter->pageToContainedHtml($page);
 
@@ -62,7 +56,7 @@ class PageExportController extends Controller
      */
     public function plainText(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $pageText = $this->exportFormatter->pageToPlainText($page);
 
         return $this->download()->directly($pageText, $pageSlug . '.txt');
@@ -75,7 +69,7 @@ class PageExportController extends Controller
      */
     public function markdown(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $pageText = $this->exportFormatter->pageToMarkdown($page);
 
         return $this->download()->directly($pageText, $pageSlug . '.md');
index a3190a0fc43ee6627ef763afd6ecc1b665e681a1..232d40668f4200d02c11e2b96e32700099e23813 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Models\PageRevision;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Repos\RevisionRepo;
 use BookStack\Entities\Tools\PageContent;
@@ -18,6 +19,7 @@ class PageRevisionController extends Controller
 {
     public function __construct(
         protected PageRepo $pageRepo,
+        protected PageQueries $pageQueries,
         protected RevisionRepo $revisionRepo,
     ) {
     }
@@ -29,7 +31,7 @@ class PageRevisionController extends Controller
      */
     public function index(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
             'id' => trans('entities.pages_revisions_sort_number')
         ]);
@@ -60,7 +62,7 @@ class PageRevisionController extends Controller
      */
     public function show(string $bookSlug, string $pageSlug, int $revisionId)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         /** @var ?PageRevision $revision */
         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
         if ($revision === null) {
@@ -89,7 +91,7 @@ class PageRevisionController extends Controller
      */
     public function changes(string $bookSlug, string $pageSlug, int $revisionId)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         /** @var ?PageRevision $revision */
         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
         if ($revision === null) {
@@ -121,7 +123,7 @@ class PageRevisionController extends Controller
      */
     public function restore(string $bookSlug, string $pageSlug, int $revisionId)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
 
         $page = $this->pageRepo->restoreRevision($page, $revisionId);
@@ -136,7 +138,7 @@ class PageRevisionController extends Controller
      */
     public function destroy(string $bookSlug, string $pageSlug, int $revId)
     {
-        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
 
         $revision = $page->revisions()->where('id', '=', $revId)->first();
@@ -162,7 +164,7 @@ class PageRevisionController extends Controller
      */
     public function destroyUserDraft(string $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
         $this->revisionRepo->deleteDraftsForCurrentUser($page);
 
         return response('', 200);
index e4e7b5680644d5c37fb514ef96ba71752db0b54d..c0b9721485642ddfcc7098b6c342c2d1332eaba3 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Entities\Controllers;
 
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Http\Controller;
@@ -9,14 +10,10 @@ use Illuminate\Http\Request;
 
 class PageTemplateController extends Controller
 {
-    protected $pageRepo;
-
-    /**
-     * PageTemplateController constructor.
-     */
-    public function __construct(PageRepo $pageRepo)
-    {
-        $this->pageRepo = $pageRepo;
+    public function __construct(
+        protected PageRepo $pageRepo,
+        protected PageQueries $pageQueries,
+    ) {
     }
 
     /**
@@ -26,7 +23,19 @@ class PageTemplateController extends Controller
     {
         $page = $request->get('page', 1);
         $search = $request->get('search', '');
-        $templates = $this->pageRepo->getTemplates(10, $page, $search);
+        $count = 10;
+
+        $query = $this->pageQueries->visibleTemplates()
+            ->orderBy('name', 'asc')
+            ->skip(($page - 1) * $count)
+            ->take($count);
+
+        if ($search) {
+            $query->where('name', 'like', '%' . $search . '%');
+        }
+
+        $templates = $query->paginate($count, ['*'], 'page', $page);
+        $templates->withPath('/templates');
 
         if ($search) {
             $templates->appends(['search' => $search]);
@@ -44,7 +53,7 @@ class PageTemplateController extends Controller
      */
     public function get(int $templateId)
     {
-        $page = $this->pageRepo->getById($templateId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($templateId);
 
         if (!$page->template) {
             throw new NotFoundException();
index 78f86a5ae5076df7c5cea7320b2c25fc657a121b..d11dde4dd73b19d39bb756076ef4d2c511ba26c5 100644 (file)
@@ -116,9 +116,9 @@ class RecycleBinController extends Controller
      *
      * @throws \Exception
      */
-    public function empty()
+    public function empty(TrashCan $trash)
     {
-        $deleteCount = (new TrashCan())->empty();
+        $deleteCount = $trash->empty();
 
         $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
         $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
index 14cb790c5c509c9283713aaa6a1b69fce4f22b8b..c1644dcf5fb10afa03ebff7d38142e5162785c9d 100644 (file)
@@ -117,20 +117,11 @@ class Book extends Entity implements HasCoverImage
     /**
      * Get the direct child items within this book.
      */
-    public function getDirectChildren(): Collection
+    public function getDirectVisibleChildren(): Collection
     {
         $pages = $this->directPages()->scopes('visible')->get();
         $chapters = $this->chapters()->scopes('visible')->get();
 
         return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
     }
-
-    /**
-     * Get a visible book by its slug.
-     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
-     */
-    public static function getBySlug(string $slug): self
-    {
-        return static::visible()->where('slug', '=', $slug)->firstOrFail();
-    }
 }
index 18735e56b72e74cdf08c7a774f0aadeae1bd7d4c..ad54fb926a9edb1158b5e07cfec62ea00af76306 100644 (file)
@@ -13,38 +13,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property int    $priority
  * @property string $book_slug
  * @property Book   $book
- *
- * @method Builder whereSlugs(string $bookSlug, string $childSlug)
  */
 abstract class BookChild extends Entity
 {
-    protected static function boot()
-    {
-        parent::boot();
-
-        // Load book slugs onto these models by default during query-time
-        static::addGlobalScope('book_slug', function (Builder $builder) {
-            $builder->addSelect(['book_slug' => function ($builder) {
-                $builder->select('slug')
-                    ->from('books')
-                    ->whereColumn('books.id', '=', 'book_id');
-            }]);
-        });
-    }
-
-    /**
-     * Scope a query to find items where the child has the given childSlug
-     * where its parent has the bookSlug.
-     */
-    public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
-    {
-        return $query->with('book')
-            ->whereHas('book', function (Builder $query) use ($bookSlug) {
-                $query->where('slug', '=', $bookSlug);
-            })
-            ->where('slug', '=', $childSlug);
-    }
-
     /**
      * Get the book this page sits in.
      */
index d3a7101116ba5e36341773b2190e08a899deba86..c926aaa647a7d75501b55880e1cd2c834990f8fc 100644 (file)
@@ -11,7 +11,6 @@ use Illuminate\Support\Collection;
  * Class Chapter.
  *
  * @property Collection<Page> $pages
- * @property string           $description
  * @property ?int             $default_template_id
  * @property ?Page            $defaultTemplate
  */
@@ -70,13 +69,4 @@ class Chapter extends BookChild
         ->orderBy('priority', 'asc')
         ->get();
     }
-
-    /**
-     * Get a visible chapter by its book and page slugs.
-     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
-     */
-    public static function getBySlugs(string $bookSlug, string $chapterSlug): self
-    {
-        return static::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
-    }
 }
index 17d6f9a016f59e7805f7743a98fb41f38e140e75..3a433338bfe18cf052bf83fdde78ae279d5d9635 100644 (file)
@@ -32,9 +32,6 @@ class Page extends BookChild
 {
     use HasFactory;
 
-    public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at', 'priority'];
-    public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at', 'priority'];
-
     protected $fillable = ['name', 'priority'];
 
     public string $textField = 'text';
@@ -145,13 +142,4 @@ class Page extends BookChild
 
         return $refreshed;
     }
-
-    /**
-     * Get a visible page by its book and page slugs.
-     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
-     */
-    public static function getBySlugs(string $bookSlug, string $pageSlug): self
-    {
-        return static::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
-    }
 }
diff --git a/app/Entities/Queries/BookQueries.php b/app/Entities/Queries/BookQueries.php
new file mode 100644 (file)
index 0000000..5346406
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Exceptions\NotFoundException;
+use Illuminate\Database\Eloquent\Builder;
+
+class BookQueries implements ProvidesEntityQueries
+{
+    protected static array $listAttributes = [
+        'id', 'slug', 'name', 'description',
+        'created_at', 'updated_at', 'image_id', 'owned_by',
+    ];
+
+    public function start(): Builder
+    {
+        return Book::query();
+    }
+
+    public function findVisibleById(int $id): ?Book
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
+    public function findVisibleByIdOrFail(int $id): Book
+    {
+        return $this->start()->scopes('visible')->findOrFail($id);
+    }
+
+    public function findVisibleBySlugOrFail(string $slug): Book
+    {
+        /** @var ?Book $book */
+        $book = $this->start()
+            ->scopes('visible')
+            ->where('slug', '=', $slug)
+            ->first();
+
+        if ($book === null) {
+            throw new NotFoundException(trans('errors.book_not_found'));
+        }
+
+        return $book;
+    }
+
+    public function visibleForList(): Builder
+    {
+        return $this->start()->scopes('visible')
+            ->select(static::$listAttributes);
+    }
+
+    public function visibleForListWithCover(): Builder
+    {
+        return $this->visibleForList()->with('cover');
+    }
+
+    public function recentlyViewedForCurrentUser(): Builder
+    {
+        return $this->visibleForList()
+            ->scopes('withLastView')
+            ->having('last_viewed_at', '>', 0)
+            ->orderBy('last_viewed_at', 'desc');
+    }
+
+    public function popularForList(): Builder
+    {
+        return $this->visibleForList()
+            ->scopes('withViewCount')
+            ->having('view_count', '>', 0)
+            ->orderBy('view_count', 'desc');
+    }
+}
diff --git a/app/Entities/Queries/BookshelfQueries.php b/app/Entities/Queries/BookshelfQueries.php
new file mode 100644 (file)
index 0000000..19717fb
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Exceptions\NotFoundException;
+use Illuminate\Database\Eloquent\Builder;
+
+class BookshelfQueries implements ProvidesEntityQueries
+{
+    protected static array $listAttributes = [
+        'id', 'slug', 'name', 'description',
+        'created_at', 'updated_at', 'image_id', 'owned_by',
+    ];
+
+    public function start(): Builder
+    {
+        return Bookshelf::query();
+    }
+
+    public function findVisibleById(int $id): ?Bookshelf
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
+    public function findVisibleByIdOrFail(int $id): Bookshelf
+    {
+        $shelf = $this->findVisibleById($id);
+
+        if (is_null($shelf)) {
+            throw new NotFoundException(trans('errors.bookshelf_not_found'));
+        }
+
+        return $shelf;
+    }
+
+    public function findVisibleBySlugOrFail(string $slug): Bookshelf
+    {
+        /** @var ?Bookshelf $shelf */
+        $shelf = $this->start()
+            ->scopes('visible')
+            ->where('slug', '=', $slug)
+            ->first();
+
+        if ($shelf === null) {
+            throw new NotFoundException(trans('errors.bookshelf_not_found'));
+        }
+
+        return $shelf;
+    }
+
+    public function visibleForList(): Builder
+    {
+        return $this->start()->scopes('visible')->select(static::$listAttributes);
+    }
+
+    public function visibleForListWithCover(): Builder
+    {
+        return $this->visibleForList()->with('cover');
+    }
+
+    public function recentlyViewedForCurrentUser(): Builder
+    {
+        return $this->visibleForList()
+            ->scopes('withLastView')
+            ->having('last_viewed_at', '>', 0)
+            ->orderBy('last_viewed_at', 'desc');
+    }
+
+    public function popularForList(): Builder
+    {
+        return $this->visibleForList()
+            ->scopes('withViewCount')
+            ->having('view_count', '>', 0)
+            ->orderBy('view_count', 'desc');
+    }
+}
diff --git a/app/Entities/Queries/ChapterQueries.php b/app/Entities/Queries/ChapterQueries.php
new file mode 100644 (file)
index 0000000..53c5bc9
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Chapter;
+use BookStack\Exceptions\NotFoundException;
+use Illuminate\Database\Eloquent\Builder;
+
+class ChapterQueries implements ProvidesEntityQueries
+{
+    protected static array $listAttributes = [
+        'id', 'slug', 'name', 'description', 'priority',
+        'book_id', 'created_at', 'updated_at', 'owned_by',
+    ];
+
+    public function start(): Builder
+    {
+        return Chapter::query();
+    }
+
+    public function findVisibleById(int $id): ?Chapter
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
+    public function findVisibleByIdOrFail(int $id): Chapter
+    {
+        return $this->start()->scopes('visible')->findOrFail($id);
+    }
+
+    public function findVisibleBySlugsOrFail(string $bookSlug, string $chapterSlug): Chapter
+    {
+        /** @var ?Chapter $chapter */
+        $chapter = $this->start()
+            ->scopes('visible')
+            ->with('book')
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            })
+            ->where('slug', '=', $chapterSlug)
+            ->first();
+
+        if (is_null($chapter)) {
+            throw new NotFoundException(trans('errors.chapter_not_found'));
+        }
+
+        return $chapter;
+    }
+
+    public function usingSlugs(string $bookSlug, string $chapterSlug): Builder
+    {
+        return $this->start()
+            ->where('slug', '=', $chapterSlug)
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            });
+    }
+
+    public function visibleForList(): Builder
+    {
+        return $this->start()
+            ->scopes('visible')
+            ->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
new file mode 100644 (file)
index 0000000..36dc6c0
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Entity;
+use Illuminate\Database\Eloquent\Builder;
+use InvalidArgumentException;
+
+class EntityQueries
+{
+    public function __construct(
+        public BookshelfQueries $shelves,
+        public BookQueries $books,
+        public ChapterQueries $chapters,
+        public PageQueries $pages,
+        public PageRevisionQueries $revisions,
+    ) {
+    }
+
+    /**
+     * 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]);
+        $queries = $this->getQueriesForType($entityType);
+
+        return $queries->findVisibleById($entityId);
+    }
+
+    /**
+     * Start a query of visible entities of the given type,
+     * suitable for listing display.
+     */
+    public function visibleForList(string $entityType): Builder
+    {
+        $queries = $this->getQueriesForType($entityType);
+        return $queries->visibleForList();
+    }
+
+    protected function getQueriesForType(string $type): ProvidesEntityQueries
+    {
+        /** @var ?ProvidesEntityQueries $queries */
+        $queries = match ($type) {
+            'page' => $this->pages,
+            'chapter' => $this->chapters,
+            'book' => $this->books,
+            'bookshelf' => $this->shelves,
+            default => null,
+        };
+
+        if (is_null($queries)) {
+            throw new InvalidArgumentException("No entity query class configured for {$type}");
+        }
+
+        return $queries;
+    }
+}
diff --git a/app/Entities/Queries/EntityQuery.php b/app/Entities/Queries/EntityQuery.php
deleted file mode 100644 (file)
index 2246e13..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-namespace BookStack\Entities\Queries;
-
-use BookStack\Entities\EntityProvider;
-use BookStack\Permissions\PermissionApplicator;
-
-abstract class EntityQuery
-{
-    protected function permissionService(): PermissionApplicator
-    {
-        return app()->make(PermissionApplicator::class);
-    }
-
-    protected function entityProvider(): EntityProvider
-    {
-        return app()->make(EntityProvider::class);
-    }
-}
diff --git a/app/Entities/Queries/PageQueries.php b/app/Entities/Queries/PageQueries.php
new file mode 100644 (file)
index 0000000..06298f4
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Exceptions\NotFoundException;
+use Illuminate\Database\Eloquent\Builder;
+
+class PageQueries implements ProvidesEntityQueries
+{
+    protected static array $contentAttributes = [
+        'name', 'id', 'slug', 'book_id', 'chapter_id', 'draft',
+        'template', 'html', 'text', 'created_at', 'updated_at', 'priority',
+        'created_by', 'updated_by', 'owned_by',
+    ];
+    protected static array $listAttributes = [
+        'name', 'id', 'slug', 'book_id', 'chapter_id', 'draft',
+        'template', 'text', 'created_at', 'updated_at', 'priority', 'owned_by',
+    ];
+
+    public function start(): Builder
+    {
+        return Page::query();
+    }
+
+    public function findVisibleById(int $id): ?Page
+    {
+        return $this->start()->scopes('visible')->find($id);
+    }
+
+    public function findVisibleByIdOrFail(int $id): Page
+    {
+        $page = $this->findVisibleById($id);
+
+        if (is_null($page)) {
+            throw new NotFoundException(trans('errors.page_not_found'));
+        }
+
+        return $page;
+    }
+
+    public function findVisibleBySlugsOrFail(string $bookSlug, string $pageSlug): Page
+    {
+        /** @var ?Page $page */
+        $page = $this->start()->with('book')
+            ->scopes('visible')
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            })
+            ->where('slug', '=', $pageSlug)
+            ->first();
+
+        if (is_null($page)) {
+            throw new NotFoundException(trans('errors.page_not_found'));
+        }
+
+        return $page;
+    }
+
+    public function usingSlugs(string $bookSlug, string $pageSlug): Builder
+    {
+        return $this->start()
+            ->where('slug', '=', $pageSlug)
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            });
+    }
+
+    public function visibleForList(): Builder
+    {
+        return $this->start()
+            ->scopes('visible')
+            ->select($this->mergeBookSlugForSelect(static::$listAttributes));
+    }
+
+    public function visibleForChapterList(int $chapterId): Builder
+    {
+        return $this->visibleForList()
+            ->where('chapter_id', '=', $chapterId)
+            ->orderBy('draft', 'desc')
+            ->orderBy('priority', 'asc');
+    }
+
+    public function visibleWithContents(): Builder
+    {
+        return $this->start()
+            ->scopes('visible')
+            ->select($this->mergeBookSlugForSelect(static::$contentAttributes));
+    }
+
+    public function currentUserDraftsForList(): Builder
+    {
+        return $this->visibleForList()
+            ->where('draft', '=', true)
+            ->where('created_by', '=', user()->id);
+    }
+
+    public function visibleTemplates(): Builder
+    {
+        return $this->visibleForList()
+            ->where('template', '=', true);
+    }
+
+    protected function mergeBookSlugForSelect(array $columns): array
+    {
+        return array_merge($columns, ['book_slug' => function ($builder) {
+            $builder->select('slug')
+                ->from('books')
+                ->whereColumn('books.id', '=', 'pages.book_id');
+        }]);
+    }
+}
diff --git a/app/Entities/Queries/PageRevisionQueries.php b/app/Entities/Queries/PageRevisionQueries.php
new file mode 100644 (file)
index 0000000..6e017a7
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\PageRevision;
+use Illuminate\Database\Eloquent\Builder;
+
+class PageRevisionQueries
+{
+    public function start(): Builder
+    {
+        return PageRevision::query();
+    }
+
+    public function findLatestVersionBySlugs(string $bookSlug, string $pageSlug): ?PageRevision
+    {
+        return PageRevision::query()
+            ->whereHas('page', function (Builder $query) {
+                $query->scopes('visible');
+            })
+            ->where('slug', '=', $pageSlug)
+            ->where('type', '=', 'version')
+            ->where('book_slug', '=', $bookSlug)
+            ->orderBy('created_at', 'desc')
+            ->first();
+    }
+
+    public function findLatestCurrentUserDraftsForPageId(int $pageId): ?PageRevision
+    {
+        /** @var ?PageRevision $revision */
+        $revision = $this->latestCurrentUserDraftsForPageId($pageId)->first();
+
+        return $revision;
+    }
+
+    public function latestCurrentUserDraftsForPageId(int $pageId): Builder
+    {
+        return $this->start()
+            ->where('created_by', '=', user()->id)
+            ->where('type', 'update_draft')
+            ->where('page_id', '=', $pageId)
+            ->orderBy('created_at', 'desc');
+    }
+}
diff --git a/app/Entities/Queries/ProvidesEntityQueries.php b/app/Entities/Queries/ProvidesEntityQueries.php
new file mode 100644 (file)
index 0000000..611d0ae
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Entities\Models\Entity;
+use Illuminate\Database\Eloquent\Builder;
+
+/**
+ * Interface for our classes which provide common queries for our
+ * entity objects. Ideally all queries for entities should run through
+ * these classes.
+ * Any added methods should return a builder instances to allow extension
+ * via building on the query, unless the method starts with 'find'
+ * in which case an entity object should be returned.
+ * (nullable unless it's a *OrFail method).
+ */
+interface ProvidesEntityQueries
+{
+    /**
+     * Start a new query for this entity type.
+     */
+    public function start(): Builder;
+
+    /**
+     * Find the entity of the given ID, or return null if not found.
+     */
+    public function findVisibleById(int $id): ?Entity;
+
+    /**
+     * Start a query for items that are visible, with selection
+     * configured for list display of this item.
+     */
+    public function visibleForList(): Builder;
+}
similarity index 80%
rename from app/Entities/Queries/Popular.php
rename to app/Entities/Queries/QueryPopular.php
index a934f346b95f43e778f1688f76f3285c723d9fe2..b2ca565eb055db8e8d0a161acd7627535f236ac4 100644 (file)
@@ -3,24 +3,32 @@
 namespace BookStack\Entities\Queries;
 
 use BookStack\Activity\Models\View;
+use BookStack\Entities\EntityProvider;
 use BookStack\Entities\Models\BookChild;
 use BookStack\Entities\Models\Entity;
+use BookStack\Permissions\PermissionApplicator;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\DB;
 
-class Popular extends EntityQuery
+class QueryPopular
 {
+    public function __construct(
+        protected PermissionApplicator $permissions,
+        protected EntityProvider $entityProvider,
+    ) {
+    }
+
     public function run(int $count, int $page, array $filterModels = null)
     {
-        $query = $this->permissionService()
+        $query = $this->permissions
             ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
             ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
             ->groupBy('viewable_id', 'viewable_type')
             ->orderBy('view_count', 'desc');
 
         if ($filterModels) {
-            $query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
+            $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
         }
 
         $entities = $query->with('viewable')
@@ -35,7 +43,7 @@ class Popular extends EntityQuery
         return $entities;
     }
 
-    protected function loadBooksForChildren(Collection $entities)
+    protected function loadBooksForChildren(Collection $entities): void
     {
         $bookChildren = $entities->filter(fn(Entity $entity) => $entity instanceof BookChild);
         $eloquent = (new \Illuminate\Database\Eloquent\Collection($bookChildren));
diff --git a/app/Entities/Queries/QueryRecentlyViewed.php b/app/Entities/Queries/QueryRecentlyViewed.php
new file mode 100644 (file)
index 0000000..f28b8f8
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace BookStack\Entities\Queries;
+
+use BookStack\Activity\Models\View;
+use BookStack\Entities\Tools\MixedEntityListLoader;
+use BookStack\Permissions\PermissionApplicator;
+use Illuminate\Support\Collection;
+
+class QueryRecentlyViewed
+{
+    public function __construct(
+        protected PermissionApplicator $permissions,
+        protected MixedEntityListLoader $listLoader,
+    ) {
+    }
+
+    public function run(int $count, int $page): Collection
+    {
+        $user = user();
+        if ($user->isGuest()) {
+            return collect();
+        }
+
+        $query = $this->permissions->restrictEntityRelationQuery(
+            View::query(),
+            'views',
+            'viewable_id',
+            'viewable_type'
+        )
+            ->orderBy('views.updated_at', 'desc')
+            ->where('user_id', '=', user()->id);
+
+        $views = $query
+            ->skip(($page - 1) * $count)
+            ->take($count)
+            ->get();
+
+        $this->listLoader->loadIntoRelations($views->all(), 'viewable', false);
+
+        return $views->pluck('viewable')->filter();
+    }
+}
similarity index 63%
rename from app/Entities/Queries/TopFavourites.php
rename to app/Entities/Queries/QueryTopFavourites.php
index a2f8d9ea1d2cdd42a93bebecf073f381bb14b672..6340e35ef18c63be91ebc925507062f34d43a1be 100644 (file)
@@ -3,10 +3,18 @@
 namespace BookStack\Entities\Queries;
 
 use BookStack\Activity\Models\Favourite;
+use BookStack\Entities\Tools\MixedEntityListLoader;
+use BookStack\Permissions\PermissionApplicator;
 use Illuminate\Database\Query\JoinClause;
 
-class TopFavourites extends EntityQuery
+class QueryTopFavourites
 {
+    public function __construct(
+        protected PermissionApplicator $permissions,
+        protected MixedEntityListLoader $listLoader,
+    ) {
+    }
+
     public function run(int $count, int $skip = 0)
     {
         $user = user();
@@ -14,7 +22,7 @@ class TopFavourites extends EntityQuery
             return collect();
         }
 
-        $query = $this->permissionService()
+        $query = $this->permissions
             ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
             ->select('favourites.*')
             ->leftJoin('views', function (JoinClause $join) {
@@ -25,11 +33,13 @@ class TopFavourites extends EntityQuery
             ->orderBy('views.views', 'desc')
             ->where('favourites.user_id', '=', user()->id);
 
-        return $query->with('favouritable')
+        $favourites = $query
             ->skip($skip)
             ->take($count)
-            ->get()
-            ->pluck('favouritable')
-            ->filter();
+            ->get();
+
+        $this->listLoader->loadIntoRelations($favourites->all(), 'favouritable', false);
+
+        return $favourites->pluck('favouritable')->filter();
     }
 }
diff --git a/app/Entities/Queries/RecentlyViewed.php b/app/Entities/Queries/RecentlyViewed.php
deleted file mode 100644 (file)
index 5895b97..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-namespace BookStack\Entities\Queries;
-
-use BookStack\Activity\Models\View;
-use Illuminate\Support\Collection;
-
-class RecentlyViewed extends EntityQuery
-{
-    public function run(int $count, int $page): Collection
-    {
-        $user = user();
-        if ($user === null || $user->isGuest()) {
-            return collect();
-        }
-
-        $query = $this->permissionService()->restrictEntityRelationQuery(
-            View::query(),
-            'views',
-            'viewable_id',
-            'viewable_type'
-        )
-            ->orderBy('views.updated_at', 'desc')
-            ->where('user_id', '=', user()->id);
-
-        return $query->with('viewable')
-            ->skip(($page - 1) * $count)
-            ->take($count)
-            ->get()
-            ->pluck('viewable')
-            ->filter();
-    }
-}
index 17208ae032a196b6780da187b675c7e24967c854..033350743e0dd824bc259254d67e80edee3c187c 100644 (file)
@@ -8,7 +8,7 @@ use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\HasCoverImage;
 use BookStack\Entities\Models\HasHtmlDescription;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\References\ReferenceStore;
 use BookStack\References\ReferenceUpdater;
@@ -23,6 +23,7 @@ class BaseRepo
         protected ImageRepo $imageRepo,
         protected ReferenceUpdater $referenceUpdater,
         protected ReferenceStore $referenceStore,
+        protected PageQueries $pageQueries,
     ) {
     }
 
@@ -125,8 +126,7 @@ class BaseRepo
             return;
         }
 
-        $templateExists = Page::query()->visible()
-            ->where('template', '=', true)
+        $templateExists = $this->pageQueries->visibleTemplates()
             ->where('id', '=', $templateId)
             ->exists();
 
index bf765b22d158f43524c4b51f077d5f5a8f109430..19d159eb1e7e9cc539a86f6031ef8004a20e70ef 100644 (file)
@@ -5,79 +5,23 @@ namespace BookStack\Entities\Repos;
 use BookStack\Activity\ActivityType;
 use BookStack\Activity\TagRepo;
 use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Page;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\ImageUploadException;
-use BookStack\Exceptions\NotFoundException;
 use BookStack\Facades\Activity;
 use BookStack\Uploads\ImageRepo;
 use Exception;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 use Illuminate\Http\UploadedFile;
-use Illuminate\Support\Collection;
 
 class BookRepo
 {
     public function __construct(
         protected BaseRepo $baseRepo,
         protected TagRepo $tagRepo,
-        protected ImageRepo $imageRepo
+        protected ImageRepo $imageRepo,
+        protected TrashCan $trashCan,
     ) {
     }
 
-    /**
-     * Get all books in a paginated format.
-     */
-    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
-    {
-        return Book::visible()->with('cover')->orderBy($sort, $order)->paginate($count);
-    }
-
-    /**
-     * Get the books that were most recently viewed by this user.
-     */
-    public function getRecentlyViewed(int $count = 20): Collection
-    {
-        return Book::visible()->withLastView()
-            ->having('last_viewed_at', '>', 0)
-            ->orderBy('last_viewed_at', 'desc')
-            ->take($count)->get();
-    }
-
-    /**
-     * Get the most popular books in the system.
-     */
-    public function getPopular(int $count = 20): Collection
-    {
-        return Book::visible()->withViewCount()
-            ->having('view_count', '>', 0)
-            ->orderBy('view_count', 'desc')
-            ->take($count)->get();
-    }
-
-    /**
-     * Get the most recently created books from the system.
-     */
-    public function getRecentlyCreated(int $count = 20): Collection
-    {
-        return Book::visible()->orderBy('created_at', 'desc')
-            ->take($count)->get();
-    }
-
-    /**
-     * Get a book by its slug.
-     */
-    public function getBySlug(string $slug): Book
-    {
-        $book = Book::visible()->where('slug', '=', $slug)->first();
-
-        if ($book === null) {
-            throw new NotFoundException(trans('errors.book_not_found'));
-        }
-
-        return $book;
-    }
-
     /**
      * Create a new book in the system.
      */
@@ -130,10 +74,9 @@ class BookRepo
      */
     public function destroy(Book $book)
     {
-        $trashCan = new TrashCan();
-        $trashCan->softDestroyBook($book);
+        $this->trashCan->softDestroyBook($book);
         Activity::add(ActivityType::BOOK_DELETE, $book);
 
-        $trashCan->autoClearOld();
+        $this->trashCan->autoClearOld();
     }
 }
index 27333b5b15c131d40b6eb1e6aa354e7a2a7ffaee..a00349ef1aeaec8cc79949932e4132ea14bc8d73 100644 (file)
@@ -3,81 +3,19 @@
 namespace BookStack\Entities\Repos;
 
 use BookStack\Activity\ActivityType;
-use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookQueries;
 use BookStack\Entities\Tools\TrashCan;
-use BookStack\Exceptions\NotFoundException;
 use BookStack\Facades\Activity;
 use Exception;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
-use Illuminate\Support\Collection;
 
 class BookshelfRepo
 {
-    protected $baseRepo;
-
-    /**
-     * BookshelfRepo constructor.
-     */
-    public function __construct(BaseRepo $baseRepo)
-    {
-        $this->baseRepo = $baseRepo;
-    }
-
-    /**
-     * Get all bookshelves in a paginated format.
-     */
-    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
-    {
-        return Bookshelf::visible()
-            ->with(['visibleBooks', 'cover'])
-            ->orderBy($sort, $order)
-            ->paginate($count);
-    }
-
-    /**
-     * Get the bookshelves that were most recently viewed by this user.
-     */
-    public function getRecentlyViewed(int $count = 20): Collection
-    {
-        return Bookshelf::visible()->withLastView()
-            ->having('last_viewed_at', '>', 0)
-            ->orderBy('last_viewed_at', 'desc')
-            ->take($count)->get();
-    }
-
-    /**
-     * Get the most popular bookshelves in the system.
-     */
-    public function getPopular(int $count = 20): Collection
-    {
-        return Bookshelf::visible()->withViewCount()
-            ->having('view_count', '>', 0)
-            ->orderBy('view_count', 'desc')
-            ->take($count)->get();
-    }
-
-    /**
-     * Get the most recently created bookshelves from the system.
-     */
-    public function getRecentlyCreated(int $count = 20): Collection
-    {
-        return Bookshelf::visible()->orderBy('created_at', 'desc')
-            ->take($count)->get();
-    }
-
-    /**
-     * Get a shelf by its slug.
-     */
-    public function getBySlug(string $slug): Bookshelf
-    {
-        $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
-
-        if ($shelf === null) {
-            throw new NotFoundException(trans('errors.bookshelf_not_found'));
-        }
-
-        return $shelf;
+    public function __construct(
+        protected BaseRepo $baseRepo,
+        protected BookQueries $bookQueries,
+        protected TrashCan $trashCan,
+    ) {
     }
 
     /**
@@ -124,7 +62,7 @@ class BookshelfRepo
             return intval($id);
         });
 
-        $syncData = Book::visible()
+        $syncData = $this->bookQueries->visibleForList()
             ->whereIn('id', $bookIds)
             ->pluck('id')
             ->mapWithKeys(function ($bookId) use ($numericIDs) {
@@ -141,9 +79,8 @@ class BookshelfRepo
      */
     public function destroy(Bookshelf $shelf)
     {
-        $trashCan = new TrashCan();
-        $trashCan->softDestroyShelf($shelf);
+        $this->trashCan->softDestroyShelf($shelf);
         Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf);
-        $trashCan->autoClearOld();
+        $this->trashCan->autoClearOld();
     }
 }
index 50b554d68dc585b4d1f3b42b16c8021936721c8e..17cbccd4133676bc44aac8aaf844478ba771e5c7 100644 (file)
@@ -4,12 +4,11 @@ namespace BookStack\Entities\Repos;
 
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Page;
 use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Tools\BookContents;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\MoveOperationException;
-use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Facades\Activity;
 use Exception;
@@ -17,26 +16,12 @@ use Exception;
 class ChapterRepo
 {
     public function __construct(
-        protected BaseRepo $baseRepo
+        protected BaseRepo $baseRepo,
+        protected EntityQueries $entityQueries,
+        protected TrashCan $trashCan,
     ) {
     }
 
-    /**
-     * Get a chapter via the slug.
-     *
-     * @throws NotFoundException
-     */
-    public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
-    {
-        $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
-
-        if ($chapter === null) {
-            throw new NotFoundException(trans('errors.chapter_not_found'));
-        }
-
-        return $chapter;
-    }
-
     /**
      * Create a new chapter in the system.
      */
@@ -75,10 +60,9 @@ class ChapterRepo
      */
     public function destroy(Chapter $chapter)
     {
-        $trashCan = new TrashCan();
-        $trashCan->softDestroyChapter($chapter);
+        $this->trashCan->softDestroyChapter($chapter);
         Activity::add(ActivityType::CHAPTER_DELETE, $chapter);
-        $trashCan->autoClearOld();
+        $this->trashCan->autoClearOld();
     }
 
     /**
@@ -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();
-    }
 }
index 85237a75219f62784160f388fa91601bcaa338db..2526b6c445d168757db794b875c5ce6263a37cc3 100644 (file)
@@ -8,114 +8,30 @@ 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;
 use BookStack\Entities\Tools\TrashCan;
 use BookStack\Exceptions\MoveOperationException;
-use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Facades\Activity;
 use BookStack\References\ReferenceStore;
 use BookStack\References\ReferenceUpdater;
 use Exception;
-use Illuminate\Pagination\LengthAwarePaginator;
 
 class PageRepo
 {
     public function __construct(
         protected BaseRepo $baseRepo,
         protected RevisionRepo $revisionRepo,
+        protected EntityQueries $entityQueries,
         protected ReferenceStore $referenceStore,
-        protected ReferenceUpdater $referenceUpdater
+        protected ReferenceUpdater $referenceUpdater,
+        protected TrashCan $trashCan,
     ) {
     }
 
-    /**
-     * Get a page by ID.
-     *
-     * @throws NotFoundException
-     */
-    public function getById(int $id, array $relations = ['book']): Page
-    {
-        /** @var Page $page */
-        $page = Page::visible()->with($relations)->find($id);
-
-        if (!$page) {
-            throw new NotFoundException(trans('errors.page_not_found'));
-        }
-
-        return $page;
-    }
-
-    /**
-     * Get a page its book and own slug.
-     *
-     * @throws NotFoundException
-     */
-    public function getBySlug(string $bookSlug, string $pageSlug): Page
-    {
-        $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
-
-        if (!$page) {
-            throw new NotFoundException(trans('errors.page_not_found'));
-        }
-
-        return $page;
-    }
-
-    /**
-     * Get a page by its old slug but checking the revisions table
-     * for the last revision that matched the given page and book slug.
-     */
-    public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
-    {
-        $revision = $this->revisionRepo->getBySlugs($bookSlug, $pageSlug);
-
-        return $revision->page ?? null;
-    }
-
-    /**
-     * Get pages that have been marked as a template.
-     */
-    public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
-    {
-        $query = Page::visible()
-            ->where('template', '=', true)
-            ->orderBy('name', 'asc')
-            ->skip(($page - 1) * $count)
-            ->take($count);
-
-        if ($search) {
-            $query->where('name', 'like', '%' . $search . '%');
-        }
-
-        $paginator = $query->paginate($count, ['*'], 'page', $page);
-        $paginator->withPath('/templates');
-
-        return $paginator;
-    }
-
-    /**
-     * Get a parent item via slugs.
-     */
-    public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
-    {
-        if ($chapterSlug !== null) {
-            return Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
-        }
-
-        return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
-    }
-
-    /**
-     * Get the draft copy of the given page for the current user.
-     */
-    public function getUserDraft(Page $page): ?PageRevision
-    {
-        return $this->revisionRepo->getLatestDraftForCurrentUser($page);
-    }
-
     /**
      * Get a new draft page belonging to the given parent entity.
      */
@@ -269,10 +185,9 @@ class PageRepo
      */
     public function destroy(Page $page)
     {
-        $trashCan = new TrashCan();
-        $trashCan->softDestroyPage($page);
+        $this->trashCan->softDestroyPage($page);
         Activity::add(ActivityType::PAGE_DELETE, $page);
-        $trashCan->autoClearOld();
+        $this->trashCan->autoClearOld();
     }
 
     /**
@@ -324,8 +239,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 +258,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.
      */
index 064327ee9456c12b64b0d6a1487715ec501cc639..daf55777ce3e9557ab282a8f678880f19bee55c3 100644 (file)
@@ -4,39 +4,13 @@ namespace BookStack\Entities\Repos;
 
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Models\PageRevision;
-use Illuminate\Database\Eloquent\Builder;
+use BookStack\Entities\Queries\PageRevisionQueries;
 
 class RevisionRepo
 {
-    /**
-     * Get a revision by its stored book and page slug values.
-     */
-    public function getBySlugs(string $bookSlug, string $pageSlug): ?PageRevision
-    {
-        /** @var ?PageRevision $revision */
-        $revision = PageRevision::query()
-            ->whereHas('page', function (Builder $query) {
-                $query->scopes('visible');
-            })
-            ->where('slug', '=', $pageSlug)
-            ->where('type', '=', 'version')
-            ->where('book_slug', '=', $bookSlug)
-            ->orderBy('created_at', 'desc')
-            ->with('page')
-            ->first();
-
-        return $revision;
-    }
-
-    /**
-     * Get the latest draft revision, for the given page, belonging to the current user.
-     */
-    public function getLatestDraftForCurrentUser(Page $page): ?PageRevision
-    {
-        /** @var ?PageRevision $revision */
-        $revision = $this->queryForCurrentUserDraft($page->id)->first();
-
-        return $revision;
+    public function __construct(
+        protected PageRevisionQueries $queries,
+    ) {
     }
 
     /**
@@ -44,7 +18,7 @@ class RevisionRepo
      */
     public function deleteDraftsForCurrentUser(Page $page): void
     {
-        $this->queryForCurrentUserDraft($page->id)->delete();
+        $this->queries->latestCurrentUserDraftsForPageId($page->id)->delete();
     }
 
     /**
@@ -53,7 +27,7 @@ class RevisionRepo
      */
     public function getNewDraftForCurrentUser(Page $page): PageRevision
     {
-        $draft = $this->getLatestDraftForCurrentUser($page);
+        $draft = $this->queries->findLatestCurrentUserDraftsForPageId($page->id);
 
         if ($draft) {
             return $draft;
@@ -116,16 +90,4 @@ class RevisionRepo
             PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
         }
     }
-
-    /**
-     * Query update draft revisions for the current user.
-     */
-    protected function queryForCurrentUserDraft(int $pageId): Builder
-    {
-        return PageRevision::query()
-            ->where('created_by', '=', user()->id)
-            ->where('type', 'update_draft')
-            ->where('page_id', '=', $pageId)
-            ->orderBy('created_at', 'desc');
-    }
 }
index f45bdfcc1b98fabcdb2d4dc774c30752a7582ef1..7fa2134b7fad60627393c334781d512542133298 100644 (file)
@@ -7,15 +7,17 @@ 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
 {
-    protected Book $book;
+    protected EntityQueries $queries;
 
-    public function __construct(Book $book)
-    {
-        $this->book = $book;
+    public function __construct(
+        protected Book $book,
+    ) {
+        $this->queries = app()->make(EntityQueries::class);
     }
 
     /**
@@ -23,10 +25,12 @@ class BookContents
      */
     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);
@@ -38,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();
@@ -87,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();
     }
 
     /**
@@ -126,7 +132,7 @@ class BookContents
 
         /** @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
@@ -279,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;
@@ -289,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;
index f7ed4b72dcb628263b5ce0395ea29d4bd440ff59..2030b050c4b19f39e01b1c6312c3e381405ff855 100644 (file)
@@ -77,7 +77,7 @@ class Cloner
         $copyBook = $this->bookRepo->create($bookDetails);
 
         // Clone contents
-        $directChildren = $original->getDirectChildren();
+        $directChildren = $original->getDirectVisibleChildren();
         foreach ($directChildren as $child) {
             if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) {
                 $this->cloneChapter($child, $copyBook, $child->name);
index 50079e3bfe885b502d332c28697625030b736827..f9a940b981b7f4261c33776c2e2897b054701e40 100644 (file)
@@ -3,20 +3,13 @@
 namespace BookStack\Entities\Tools;
 
 use BookStack\App\Model;
-use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Queries\EntityQueries;
 use Illuminate\Database\Eloquent\Relations\Relation;
 
 class MixedEntityListLoader
 {
-    protected array $listAttributes = [
-        'page'      => ['id', 'name', 'slug', 'book_id', 'chapter_id', 'text', 'draft'],
-        'chapter'   => ['id', 'name', 'slug', 'book_id', 'description'],
-        'book'      => ['id', 'name', 'slug', 'description'],
-        'bookshelf' => ['id', 'name', 'slug', 'description'],
-    ];
-
     public function __construct(
-        protected EntityProvider $entityProvider
+        protected EntityQueries $queries,
     ) {
     }
 
@@ -26,7 +19,7 @@ class MixedEntityListLoader
      * This will look for a model id and type via 'name_id' and 'name_type'.
      * @param Model[] $relations
      */
-    public function loadIntoRelations(array $relations, string $relationName): void
+    public function loadIntoRelations(array $relations, string $relationName, bool $loadParents): void
     {
         $idsByType = [];
         foreach ($relations as $relation) {
@@ -40,7 +33,7 @@ class MixedEntityListLoader
             $idsByType[$type][] = $id;
         }
 
-        $modelMap = $this->idsByTypeToModelMap($idsByType);
+        $modelMap = $this->idsByTypeToModelMap($idsByType, $loadParents);
 
         foreach ($relations as $relation) {
             $type = $relation->getAttribute($relationName . '_type');
@@ -56,21 +49,14 @@ class MixedEntityListLoader
      * @param array<string, int[]> $idsByType
      * @return array<string, array<int, Model>>
      */
-    protected function idsByTypeToModelMap(array $idsByType): array
+    protected function idsByTypeToModelMap(array $idsByType, bool $eagerLoadParents): array
     {
         $modelMap = [];
 
         foreach ($idsByType as $type => $ids) {
-            if (!isset($this->listAttributes[$type])) {
-                continue;
-            }
-
-            $instance = $this->entityProvider->get($type);
-            $models = $instance->newQuery()
-                ->select($this->listAttributes[$type])
-                ->scopes('visible')
+            $models = $this->queries->visibleForList($type)
                 ->whereIn('id', $ids)
-                ->with($this->getRelationsToEagerLoad($type))
+                ->with($eagerLoadParents ? $this->getRelationsToEagerLoad($type) : [])
                 ->get();
 
             if (count($models) > 0) {
index 6a89ff626317aaade96c87ceab584703eeb97b06..4f68b828fc56e794bfd196535e338d3632df4d98 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Entities\Tools;
 
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Facades\Theme;
@@ -21,9 +22,12 @@ use Illuminate\Support\Str;
 
 class PageContent
 {
+    protected PageQueries $pageQueries;
+
     public function __construct(
         protected Page $page
     ) {
+        $this->pageQueries = app()->make(PageQueries::class);
     }
 
     /**
@@ -325,13 +329,14 @@ class PageContent
     protected function getContentProviderClosure(bool $blankIncludes): Closure
     {
         $contextPage = $this->page;
+        $queries = $this->pageQueries;
 
-        return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage): PageIncludeContent {
+        return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage, $queries): PageIncludeContent {
             if ($blankIncludes) {
                 return PageIncludeContent::fromHtmlAndTag('', $tag);
             }
 
-            $matchedPage = Page::visible()->find($tag->getPageId());
+            $matchedPage = $queries->findVisibleById($tag->getPageId());
             $content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag);
 
             if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) {
index 3c7c9e2eaf0954e04c73089d34ecea1202c942f0..f0bd235897d97017e45f0eb22fd37142cd1fa54d 100644 (file)
@@ -4,7 +4,7 @@ namespace BookStack\Entities\Tools;
 
 use BookStack\Activity\Tools\CommentTree;
 use BookStack\Entities\Models\Page;
-use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
 use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
 
@@ -15,7 +15,7 @@ class PageEditorData
 
     public function __construct(
         protected Page $page,
-        protected PageRepo $pageRepo,
+        protected EntityQueries $queries,
         protected string $requestedEditor
     ) {
         $this->viewData = $this->build();
@@ -35,7 +35,12 @@ class PageEditorData
     {
         $page = clone $this->page;
         $isDraft = boolval($this->page->draft);
-        $templates = $this->pageRepo->getTemplates(10);
+        $templates = $this->queries->pages->visibleTemplates()
+            ->orderBy('name', 'asc')
+            ->take(10)
+            ->paginate()
+            ->withPath('/templates');
+
         $draftsEnabled = auth()->check();
 
         $isDraftRevision = false;
@@ -47,8 +52,8 @@ class PageEditorData
         }
 
         // Check for a current draft version for this user
-        $userDraft = $this->pageRepo->getUserDraft($page);
-        if ($userDraft !== null) {
+        $userDraft = $this->queries->revisions->findLatestCurrentUserDraftsForPageId($page->id);
+        if (!is_null($userDraft)) {
             $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
             $isDraftRevision = true;
             $this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
index 50c7047d9e39607c2ba3459fc5e6e845a33dbc13..5ed3348785450830eeca11196866f712732a095e 100644 (file)
@@ -4,10 +4,16 @@ namespace BookStack\Entities\Tools;
 
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
 
 class ShelfContext
 {
-    protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
+    protected string $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
+
+    public function __construct(
+        protected BookshelfQueries $shelfQueries,
+    ) {
+    }
 
     /**
      * Get the current bookshelf context for the given book.
@@ -20,8 +26,7 @@ class ShelfContext
             return null;
         }
 
-        /** @var Bookshelf $shelf */
-        $shelf = Bookshelf::visible()->find($contextBookshelfId);
+        $shelf = $this->shelfQueries->findVisibleById($contextBookshelfId);
         $shelfContainsBook = $shelf && $shelf->contains($book);
 
         return $shelfContainsBook ? $shelf : null;
@@ -30,7 +35,7 @@ class ShelfContext
     /**
      * Store the current contextual shelf ID.
      */
-    public function setShelfContext(int $shelfId)
+    public function setShelfContext(int $shelfId): void
     {
         session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
     }
@@ -38,7 +43,7 @@ class ShelfContext
     /**
      * Clear the session stored shelf context id.
      */
-    public function clearShelfContext()
+    public function clearShelfContext(): void
     {
         session()->forget($this->KEY_SHELF_CONTEXT_ID);
     }
index 617ef4a620df810f9369e8fd8f6e49abed461304..34d0fc6b07e465c3bdc85dbaf7174fdc668ecf24 100644 (file)
@@ -7,10 +7,17 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use Illuminate\Support\Collection;
 
 class SiblingFetcher
 {
+    public function __construct(
+        protected EntityQueries $queries,
+        protected ShelfContext $shelfContext,
+    ) {
+    }
+
     /**
      * Search among the siblings of the entity of given type and id.
      */
@@ -26,23 +33,23 @@ class SiblingFetcher
 
         // Page in book or chapter
         if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) {
-            $entities = $entity->book->getDirectChildren();
+            $entities = $entity->book->getDirectVisibleChildren();
         }
 
         // Book
         // Gets just the books in a shelf if shelf is in context
         if ($entity instanceof Book) {
-            $contextShelf = (new ShelfContext())->getContextualShelfForBook($entity);
+            $contextShelf = $this->shelfContext->getContextualShelfForBook($entity);
             if ($contextShelf) {
                 $entities = $contextShelf->visibleBooks()->get();
             } else {
-                $entities = Book::visible()->get();
+                $entities = $this->queries->books->visibleForList()->get();
             }
         }
 
         // Shelf
         if ($entity instanceof Bookshelf) {
-            $entities = Bookshelf::visible()->get();
+            $entities = $this->queries->shelves->visibleForList()->get();
         }
 
         return $entities;
index 8e9f010df0ff6fb5d5ab37e937e0481811d3f3df..39c982cdc92a25219b0df21bfaadac9c4b52a372 100644 (file)
@@ -10,6 +10,7 @@ use BookStack\Entities\Models\Deletion;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\HasCoverImage;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Exceptions\NotifyException;
 use BookStack\Facades\Activity;
 use BookStack\Uploads\AttachmentService;
@@ -20,6 +21,11 @@ use Illuminate\Support\Carbon;
 
 class TrashCan
 {
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
+
     /**
      * Send a shelf to the recycle bin.
      *
@@ -203,11 +209,13 @@ class TrashCan
         }
 
         // Remove book template usages
-        Book::query()->where('default_template_id', '=', $page->id)
+        $this->queries->books->start()
+            ->where('default_template_id', '=', $page->id)
             ->update(['default_template_id' => null]);
 
         // Remove chapter template usages
-        Chapter::query()->where('default_template_id', '=', $page->id)
+        $this->queries->chapters->start()
+            ->where('default_template_id', '=', $page->id)
             ->update(['default_template_id' => null]);
 
         $page->forceDelete();
index 8c961fb13061bc7f4169419fd13841c1ad363c79..c2922cdc9611481b1ba58a64d968f6e9d6dd18a8 100644 (file)
@@ -4,10 +4,10 @@ namespace BookStack\Permissions;
 
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\BookChild;
-use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Permissions\Models\JointPermission;
 use BookStack\Users\Models\Role;
 use Illuminate\Database\Eloquent\Builder;
@@ -20,6 +20,12 @@ use Illuminate\Support\Facades\DB;
  */
 class JointPermissionBuilder
 {
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
+
+
     /**
      * Re-generate all entity permission from scratch.
      */
@@ -36,7 +42,7 @@ class JointPermissionBuilder
         });
 
         // Chunk through all bookshelves
-        Bookshelf::query()->withTrashed()->select(['id', 'owned_by'])
+        $this->queries->shelves->start()->withTrashed()->select(['id', 'owned_by'])
             ->chunk(50, function (EloquentCollection $shelves) use ($roles) {
                 $this->createManyJointPermissions($shelves->all(), $roles);
             });
@@ -88,7 +94,7 @@ class JointPermissionBuilder
         });
 
         // Chunk through all bookshelves
-        Bookshelf::query()->select(['id', 'owned_by'])
+        $this->queries->shelves->start()->select(['id', 'owned_by'])
             ->chunk(100, function ($shelves) use ($roles) {
                 $this->createManyJointPermissions($shelves->all(), $roles);
             });
@@ -99,7 +105,7 @@ class JointPermissionBuilder
      */
     protected function bookFetchQuery(): Builder
     {
-        return Book::query()->withTrashed()
+        return $this->queries->books->start()->withTrashed()
             ->select(['id', 'owned_by'])->with([
                 'chapters' => function ($query) {
                     $query->withTrashed()->select(['id', 'owned_by', 'book_id']);
index f2014ea73f6e50e459d588fc29c09800d3a68d1c..5d2035870bccd79ba4328ec973a18a4140201475 100644 (file)
@@ -2,10 +2,7 @@
 
 namespace BookStack\Permissions;
 
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Entities\Tools\PermissionsUpdater;
 use BookStack\Http\Controller;
 use BookStack\Permissions\Models\EntityPermission;
@@ -14,19 +11,18 @@ use Illuminate\Http\Request;
 
 class PermissionsController extends Controller
 {
-    protected PermissionsUpdater $permissionsUpdater;
-
-    public function __construct(PermissionsUpdater $permissionsUpdater)
-    {
-        $this->permissionsUpdater = $permissionsUpdater;
+    public function __construct(
+        protected PermissionsUpdater $permissionsUpdater,
+        protected EntityQueries $queries,
+    ) {
     }
 
     /**
-     * Show the Permissions view for a page.
+     * Show the permissions view for a page.
      */
     public function showForPage(string $bookSlug, string $pageSlug)
     {
-        $page = Page::getBySlugs($bookSlug, $pageSlug);
+        $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
 
         $this->setPageTitle(trans('entities.pages_permissions'));
@@ -41,7 +37,7 @@ class PermissionsController extends Controller
      */
     public function updateForPage(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = Page::getBySlugs($bookSlug, $pageSlug);
+        $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
 
         $this->permissionsUpdater->updateFromPermissionsForm($page, $request);
@@ -52,11 +48,11 @@ class PermissionsController extends Controller
     }
 
     /**
-     * Show the Restrictions view for a chapter.
+     * Show the permissions view for a chapter.
      */
     public function showForChapter(string $bookSlug, string $chapterSlug)
     {
-        $chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
+        $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
 
         $this->setPageTitle(trans('entities.chapters_permissions'));
@@ -67,11 +63,11 @@ class PermissionsController extends Controller
     }
 
     /**
-     * Set the restrictions for a chapter.
+     * Set the permissions for a chapter.
      */
     public function updateForChapter(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
+        $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
 
         $this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
@@ -86,7 +82,7 @@ class PermissionsController extends Controller
      */
     public function showForBook(string $slug)
     {
-        $book = Book::getBySlug($slug);
+        $book = $this->queries->books->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('restrictions-manage', $book);
 
         $this->setPageTitle(trans('entities.books_permissions'));
@@ -97,11 +93,11 @@ class PermissionsController extends Controller
     }
 
     /**
-     * Set the restrictions for a book.
+     * Set the permissions for a book.
      */
     public function updateForBook(Request $request, string $slug)
     {
-        $book = Book::getBySlug($slug);
+        $book = $this->queries->books->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('restrictions-manage', $book);
 
         $this->permissionsUpdater->updateFromPermissionsForm($book, $request);
@@ -116,7 +112,7 @@ class PermissionsController extends Controller
      */
     public function showForShelf(string $slug)
     {
-        $shelf = Bookshelf::getBySlug($slug);
+        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
         $this->setPageTitle(trans('entities.shelves_permissions'));
@@ -131,7 +127,7 @@ class PermissionsController extends Controller
      */
     public function updateForShelf(Request $request, string $slug)
     {
-        $shelf = Bookshelf::getBySlug($slug);
+        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
         $this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
@@ -146,7 +142,7 @@ class PermissionsController extends Controller
      */
     public function copyShelfPermissionsToBooks(string $slug)
     {
-        $shelf = Bookshelf::getBySlug($slug);
+        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
         $updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
index b9c3ad205aea12a2f2d38e4199d5506b64b04222..1fd4c1b3e3c2401830c904fd2efd946797ea85b1 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\References;
 
 use BookStack\App\Model;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\References\ModelResolvers\BookLinkModelResolver;
 use BookStack\References\ModelResolvers\BookshelfLinkModelResolver;
 use BookStack\References\ModelResolvers\ChapterLinkModelResolver;
@@ -85,12 +86,14 @@ class CrossLinkParser
      */
     public static function createWithEntityResolvers(): self
     {
+        $queries = app()->make(EntityQueries::class);
+
         return new self([
-            new PagePermalinkModelResolver(),
-            new PageLinkModelResolver(),
-            new ChapterLinkModelResolver(),
-            new BookLinkModelResolver(),
-            new BookshelfLinkModelResolver(),
+            new PagePermalinkModelResolver($queries->pages),
+            new PageLinkModelResolver($queries->pages),
+            new ChapterLinkModelResolver($queries->chapters),
+            new BookLinkModelResolver($queries->books),
+            new BookshelfLinkModelResolver($queries->shelves),
         ]);
     }
 }
index d404fe2fd84dd9d2596687d70e3f8fa0fe18be43..a671fbf570062e836aee77185473804a58b7507d 100644 (file)
@@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
 
 use BookStack\App\Model;
 use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\BookQueries;
 
 class BookLinkModelResolver implements CrossLinkModelResolver
 {
+    public function __construct(
+        protected BookQueries $queries
+    ) {
+    }
+
     public function resolve(string $link): ?Model
     {
         $pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
@@ -19,7 +25,7 @@ class BookLinkModelResolver implements CrossLinkModelResolver
         $bookSlug = $matches[1];
 
         /** @var ?Book $model */
-        $model = Book::query()->where('slug', '=', $bookSlug)->first(['id']);
+        $model = $this->queries->start()->where('slug', '=', $bookSlug)->first(['id']);
 
         return $model;
     }
index cc9df034e6055e001bfffaa01e8db9ace4285f68..d79c760e070b38c9c790706088a8d55cefecab15 100644 (file)
@@ -4,9 +4,14 @@ namespace BookStack\References\ModelResolvers;
 
 use BookStack\App\Model;
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
 
 class BookshelfLinkModelResolver implements CrossLinkModelResolver
 {
+    public function __construct(
+        protected BookshelfQueries $queries
+    ) {
+    }
     public function resolve(string $link): ?Model
     {
         $pattern = '/^' . preg_quote(url('/shelves'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
@@ -19,7 +24,7 @@ class BookshelfLinkModelResolver implements CrossLinkModelResolver
         $shelfSlug = $matches[1];
 
         /** @var ?Bookshelf $model */
-        $model = Bookshelf::query()->where('slug', '=', $shelfSlug)->first(['id']);
+        $model = $this->queries->start()->where('slug', '=', $shelfSlug)->first(['id']);
 
         return $model;
     }
index 4733c68eead62c34529683143a4ac88638566038..318b5ed35f41c014a85dae3bd49a9ed528db65a7 100644 (file)
@@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
 
 use BookStack\App\Model;
 use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\ChapterQueries;
 
 class ChapterLinkModelResolver implements CrossLinkModelResolver
 {
+    public function __construct(
+        protected ChapterQueries $queries
+    ) {
+    }
+
     public function resolve(string $link): ?Model
     {
         $pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/chapter\/' . '([\w-]+)' . '([#?\/]|$)/';
@@ -20,7 +26,7 @@ class ChapterLinkModelResolver implements CrossLinkModelResolver
         $chapterSlug = $matches[2];
 
         /** @var ?Chapter $model */
-        $model = Chapter::query()->whereSlugs($bookSlug, $chapterSlug)->first(['id']);
+        $model = $this->queries->usingSlugs($bookSlug, $chapterSlug)->first(['id']);
 
         return $model;
     }
index 736382becdd1cc38b849b973512f9a81c3b54fec..9a01416a4453fb1417b5c9ec6c2b52339ba9630a 100644 (file)
@@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
 
 use BookStack\App\Model;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 
 class PageLinkModelResolver implements CrossLinkModelResolver
 {
+    public function __construct(
+        protected PageQueries $queries
+    ) {
+    }
+
     public function resolve(string $link): ?Model
     {
         $pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/page\/' . '([\w-]+)' . '([#?\/]|$)/';
@@ -20,7 +26,7 @@ class PageLinkModelResolver implements CrossLinkModelResolver
         $pageSlug = $matches[2];
 
         /** @var ?Page $model */
-        $model = Page::query()->whereSlugs($bookSlug, $pageSlug)->first(['id']);
+        $model = $this->queries->usingSlugs($bookSlug, $pageSlug)->first(['id']);
 
         return $model;
     }
index 9ed00b36dd4c3326ed928163e2007049ac7151ea..59d509f3399b972ff576cca417e160171f96bd0b 100644 (file)
@@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
 
 use BookStack\App\Model;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 
 class PagePermalinkModelResolver implements CrossLinkModelResolver
 {
+    public function __construct(
+        protected PageQueries $queries
+    ) {
+    }
+
     public function resolve(string $link): ?Model
     {
         $pattern = '/^' . preg_quote(url('/link'), '/') . '\/(\d+)/';
@@ -18,7 +24,7 @@ class PagePermalinkModelResolver implements CrossLinkModelResolver
 
         $id = intval($matches[1]);
         /** @var ?Page $model */
-        $model = Page::query()->find($id, ['id']);
+        $model = $this->queries->start()->find($id, ['id']);
 
         return $model;
     }
index 991f47225b8e044094ba31e3ebf4b3fdd09cd036..2fe86fd594b9c8a95cf692401074b13839395175 100644 (file)
@@ -2,16 +2,14 @@
 
 namespace BookStack\References;
 
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Http\Controller;
 
 class ReferenceController extends Controller
 {
     public function __construct(
-        protected ReferenceFetcher $referenceFetcher
+        protected ReferenceFetcher $referenceFetcher,
+        protected EntityQueries $queries,
     ) {
     }
 
@@ -20,7 +18,7 @@ class ReferenceController extends Controller
      */
     public function page(string $bookSlug, string $pageSlug)
     {
-        $page = Page::getBySlugs($bookSlug, $pageSlug);
+        $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         $references = $this->referenceFetcher->getReferencesToEntity($page);
 
         return view('pages.references', [
@@ -34,7 +32,7 @@ class ReferenceController extends Controller
      */
     public function chapter(string $bookSlug, string $chapterSlug)
     {
-        $chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
+        $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
         $references = $this->referenceFetcher->getReferencesToEntity($chapter);
 
         return view('chapters.references', [
@@ -48,7 +46,7 @@ class ReferenceController extends Controller
      */
     public function book(string $slug)
     {
-        $book = Book::getBySlug($slug);
+        $book = $this->queries->books->findVisibleBySlugOrFail($slug);
         $references = $this->referenceFetcher->getReferencesToEntity($book);
 
         return view('books.references', [
@@ -62,7 +60,7 @@ class ReferenceController extends Controller
      */
     public function shelf(string $slug)
     {
-        $shelf = Bookshelf::getBySlug($slug);
+        $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
         $references = $this->referenceFetcher->getReferencesToEntity($shelf);
 
         return view('shelves.references', [
index 0d9883a3edcbcc057136a3273cd5433055286f55..655ea7c09eb2bf8b0c21b7c30f0345b7ab0e3ea7 100644 (file)
@@ -23,7 +23,7 @@ class ReferenceFetcher
     public function getReferencesToEntity(Entity $entity): Collection
     {
         $references = $this->queryReferencesToEntity($entity)->get();
-        $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from');
+        $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', true);
 
         return $references;
     }
index 6cf12a57920631ecebdff6fdddf01938d80f1ac6..2fce6a3d53fb86e14e1b773f4126f0b1ba456fb2 100644 (file)
@@ -2,8 +2,8 @@
 
 namespace BookStack\Search;
 
-use BookStack\Entities\Models\Page;
-use BookStack\Entities\Queries\Popular;
+use BookStack\Entities\Queries\PageQueries;
+use BookStack\Entities\Queries\QueryPopular;
 use BookStack\Entities\Tools\SiblingFetcher;
 use BookStack\Http\Controller;
 use Illuminate\Http\Request;
@@ -11,7 +11,8 @@ use Illuminate\Http\Request;
 class SearchController extends Controller
 {
     public function __construct(
-        protected SearchRunner $searchRunner
+        protected SearchRunner $searchRunner,
+        protected PageQueries $pageQueries,
     ) {
     }
 
@@ -66,7 +67,7 @@ class SearchController extends Controller
      * Search for a list of entities and return a partial HTML response of matching entities.
      * Returns the most popular entities if no search is provided.
      */
-    public function searchForSelector(Request $request)
+    public function searchForSelector(Request $request, QueryPopular $queryPopular)
     {
         $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
         $searchTerm = $request->get('term', false);
@@ -77,7 +78,7 @@ class SearchController extends Controller
             $searchTerm .= ' {type:' . implode('|', $entityTypes) . '}';
             $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results'];
         } else {
-            $entities = (new Popular())->run(20, 0, $entityTypes);
+            $entities = $queryPopular->run(20, 0, $entityTypes);
         }
 
         return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);
@@ -95,12 +96,11 @@ class SearchController extends Controller
             $searchOptions->setFilter('is_template');
             $entities = $this->searchRunner->searchEntities($searchOptions, 'page', 1, 20)['results'];
         } else {
-            $entities = Page::visible()
-                ->where('template', '=', true)
+            $entities = $this->pageQueries->visibleTemplates()
                 ->where('draft', '=', false)
                 ->orderBy('updated_at', 'desc')
                 ->take(20)
-                ->get(Page::$listAttributes);
+                ->get();
         }
 
         return view('search.parts.entity-selector-list', [
@@ -130,12 +130,12 @@ class SearchController extends Controller
     /**
      * Search siblings items in the system.
      */
-    public function searchSiblings(Request $request)
+    public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
     {
         $type = $request->get('entity_type', null);
         $id = $request->get('entity_id', null);
 
-        $entities = (new SiblingFetcher())->fetch($type, $id);
+        $entities = $siblingFetcher->fetch($type, $id);
 
         return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
     }
index aac9d10005b64f23768961356890e21ea37cde35..94518dbf7b263d0594e6427a413702b063c82de2 100644 (file)
@@ -3,9 +3,9 @@
 namespace BookStack\Search;
 
 use BookStack\Entities\EntityProvider;
-use BookStack\Entities\Models\BookChild;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Permissions\PermissionApplicator;
 use BookStack\Users\Models\User;
 use Illuminate\Database\Connection;
@@ -20,9 +20,6 @@ use SplObjectStorage;
 
 class SearchRunner
 {
-    protected EntityProvider $entityProvider;
-    protected PermissionApplicator $permissions;
-
     /**
      * Acceptable operators to be used in a query.
      *
@@ -38,10 +35,11 @@ class SearchRunner
      */
     protected $termAdjustmentCache;
 
-    public function __construct(EntityProvider $entityProvider, PermissionApplicator $permissions)
-    {
-        $this->entityProvider = $entityProvider;
-        $this->permissions = $permissions;
+    public function __construct(
+        protected EntityProvider $entityProvider,
+        protected PermissionApplicator $permissions,
+        protected EntityQueries $entityQueries,
+    ) {
         $this->termAdjustmentCache = new SplObjectStorage();
     }
 
@@ -72,10 +70,9 @@ class SearchRunner
                 continue;
             }
 
-            $entityModelInstance = $this->entityProvider->get($entityType);
-            $searchQuery = $this->buildQuery($searchOpts, $entityModelInstance);
+            $searchQuery = $this->buildQuery($searchOpts, $entityType);
             $entityTotal = $searchQuery->count();
-            $searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityModelInstance, $page, $count);
+            $searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityType, $page, $count);
 
             if ($entityTotal > ($page * $count)) {
                 $hasMore = true;
@@ -108,8 +105,7 @@ class SearchRunner
                 continue;
             }
 
-            $entityModelInstance = $this->entityProvider->get($entityType);
-            $search = $this->buildQuery($opts, $entityModelInstance)->where('book_id', '=', $bookId)->take(20)->get();
+            $search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
             $results = $results->merge($search);
         }
 
@@ -122,8 +118,7 @@ class SearchRunner
     public function searchChapter(int $chapterId, string $searchString): Collection
     {
         $opts = SearchOptions::fromString($searchString);
-        $entityModelInstance = $this->entityProvider->get('page');
-        $pages = $this->buildQuery($opts, $entityModelInstance)->where('chapter_id', '=', $chapterId)->take(20)->get();
+        $pages = $this->buildQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
 
         return $pages->sortByDesc('score');
     }
@@ -131,17 +126,17 @@ class SearchRunner
     /**
      * Get a page of result data from the given query based on the provided page parameters.
      */
-    protected function getPageOfDataFromQuery(EloquentBuilder $query, Entity $entityModelInstance, int $page = 1, int $count = 20): EloquentCollection
+    protected function getPageOfDataFromQuery(EloquentBuilder $query, string $entityType, int $page = 1, int $count = 20): EloquentCollection
     {
         $relations = ['tags'];
 
-        if ($entityModelInstance instanceof BookChild) {
+        if ($entityType === 'page' || $entityType === 'chapter') {
             $relations['book'] = function (BelongsTo $query) {
                 $query->scopes('visible');
             };
         }
 
-        if ($entityModelInstance instanceof Page) {
+        if ($entityType === 'page') {
             $relations['chapter'] = function (BelongsTo $query) {
                 $query->scopes('visible');
             };
@@ -157,18 +152,13 @@ class SearchRunner
     /**
      * Create a search query for an entity.
      */
-    protected function buildQuery(SearchOptions $searchOpts, Entity $entityModelInstance): EloquentBuilder
+    protected function buildQuery(SearchOptions $searchOpts, string $entityType): EloquentBuilder
     {
-        $entityQuery = $entityModelInstance->newQuery()->scopes('visible');
-
-        if ($entityModelInstance instanceof Page) {
-            $entityQuery->select(array_merge($entityModelInstance::$listAttributes, ['owned_by']));
-        } else {
-            $entityQuery->select(['*']);
-        }
+        $entityModelInstance = $this->entityProvider->get($entityType);
+        $entityQuery = $this->entityQueries->visibleForList($entityType);
 
         // Handle normal search terms
-        $this->applyTermSearch($entityQuery, $searchOpts, $entityModelInstance);
+        $this->applyTermSearch($entityQuery, $searchOpts, $entityType);
 
         // Handle exact term matching
         foreach ($searchOpts->exacts as $inputTerm) {
@@ -198,7 +188,7 @@ class SearchRunner
     /**
      * For the given search query, apply the queries for handling the regular search terms.
      */
-    protected function applyTermSearch(EloquentBuilder $entityQuery, SearchOptions $options, Entity $entity): void
+    protected function applyTermSearch(EloquentBuilder $entityQuery, SearchOptions $options, string $entityType): void
     {
         $terms = $options->searches;
         if (count($terms) === 0) {
@@ -216,7 +206,7 @@ class SearchRunner
 
         $subQuery->addBinding($scoreSelect['bindings'], 'select');
 
-        $subQuery->where('entity_type', '=', $entity->getMorphClass());
+        $subQuery->where('entity_type', '=', $entityType);
         $subQuery->where(function (Builder $query) use ($terms) {
             foreach ($terms as $inputTerm) {
                 $inputTerm = str_replace('\\', '\\\\', $inputTerm);
index 62eeecf39a9ca961ec53c4b18d854190de5ea601..0382ae08a6d18c41b530fbf7c0a9f7cd12eb367b 100644 (file)
@@ -14,7 +14,7 @@ class MaintenanceController extends Controller
     /**
      * Show the page for application maintenance.
      */
-    public function index()
+    public function index(TrashCan $trashCan)
     {
         $this->checkPermission('settings-manage');
         $this->setPageTitle(trans('settings.maint'));
@@ -23,7 +23,7 @@ class MaintenanceController extends Controller
         $version = trim(file_get_contents(base_path('version')));
 
         // Recycle bin details
-        $recycleStats = (new TrashCan())->getTrashedCounts();
+        $recycleStats = $trashCan->getTrashedCounts();
 
         return view('settings.maintenance', [
             'version'      => $version,
index 72f78e347bf54cd61d5f084d3fd7a456e6d07c0d..bd319fbd795af717c4c026d63b9b8bc58ea7fabd 100644 (file)
@@ -4,7 +4,6 @@ namespace BookStack\Uploads;
 
 use BookStack\Exceptions\FileUploadException;
 use Exception;
-use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Contracts\Filesystem\Filesystem as Storage;
 use Illuminate\Filesystem\FilesystemManager;
 use Illuminate\Support\Facades\Log;
index 2e6d1620575d73455487953647107571d9825e9d..9040ba6d3a98003b550301f248632c62327d6a92 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Uploads\Controllers;
 
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Exceptions\FileUploadException;
 use BookStack\Http\ApiController;
 use BookStack\Uploads\Attachment;
@@ -15,7 +15,8 @@ use Illuminate\Validation\ValidationException;
 class AttachmentApiController extends ApiController
 {
     public function __construct(
-        protected AttachmentService $attachmentService
+        protected AttachmentService $attachmentService,
+        protected PageQueries $pageQueries,
     ) {
     }
 
@@ -48,7 +49,7 @@ class AttachmentApiController extends ApiController
         $requestData = $this->validate($request, $this->rules()['create']);
 
         $pageId = $request->get('uploaded_to');
-        $page = Page::visible()->findOrFail($pageId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
         if ($request->hasFile('file')) {
@@ -132,7 +133,7 @@ class AttachmentApiController extends ApiController
         $page = $attachment->page;
         if ($requestData['uploaded_to'] ?? false) {
             $pageId = $request->get('uploaded_to');
-            $page = Page::visible()->findOrFail($pageId);
+            $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
             $attachment->uploaded_to = $requestData['uploaded_to'];
         }
 
index e61c1033884586d10957236b2bfe2668cfa690aa..809cdfa581f61ec0a40c458ea95459965ec1cbe3 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Uploads\Controllers;
 
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\FileUploadException;
 use BookStack\Exceptions\NotFoundException;
@@ -18,6 +19,7 @@ class AttachmentController extends Controller
 {
     public function __construct(
         protected AttachmentService $attachmentService,
+        protected PageQueries $pageQueries,
         protected PageRepo $pageRepo
     ) {
     }
@@ -36,7 +38,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -152,7 +154,7 @@ class AttachmentController extends Controller
             ]), 422);
         }
 
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -173,7 +175,7 @@ class AttachmentController extends Controller
      */
     public function listForPage(int $pageId)
     {
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-view', $page);
 
         return view('attachments.manager-list', [
@@ -192,7 +194,7 @@ class AttachmentController extends Controller
         $this->validate($request, [
             'order' => ['required', 'array'],
         ]);
-        $page = $this->pageRepo->getById($pageId);
+        $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
         $attachmentOrder = $request->get('order');
@@ -213,7 +215,7 @@ class AttachmentController extends Controller
         $attachment = Attachment::query()->findOrFail($attachmentId);
 
         try {
-            $page = $this->pageRepo->getById($attachment->uploaded_to);
+            $page = $this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);
         } catch (NotFoundException $exception) {
             throw new NotFoundException(trans('errors.attachment_not_found'));
         }
index 258f2bef6bda19c48e58a12b9d46280084d9997e..1bc9da2d7245ac1be0f3f9a8bf180db7d973db2b 100644 (file)
@@ -8,8 +8,6 @@ use BookStack\Uploads\ImageRepo;
 use BookStack\Uploads\ImageResizer;
 use BookStack\Util\OutOfMemoryHandler;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\App;
-use Illuminate\Support\Facades\Log;
 use Illuminate\Validation\ValidationException;
 
 class GalleryImageController extends Controller
index ec96e4593bc2fa48f71e0e221a5c27be69278156..6d4657a7a5a10f1e202c2d02dab5a1dc6a0b3c8e 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Uploads\Controllers;
 
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Http\ApiController;
 use BookStack\Uploads\Image;
 use BookStack\Uploads\ImageRepo;
@@ -18,6 +18,7 @@ class ImageGalleryApiController extends ApiController
     public function __construct(
         protected ImageRepo $imageRepo,
         protected ImageResizer $imageResizer,
+        protected PageQueries $pageQueries,
     ) {
     }
 
@@ -66,9 +67,9 @@ class ImageGalleryApiController extends ApiController
     {
         $this->checkPermission('image-create-all');
         $data = $this->validate($request, $this->rules()['create']);
-        Page::visible()->findOrFail($data['uploaded_to']);
+        $page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']);
 
-        $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']);
+        $image = $this->imageRepo->saveNew($data['image'], $data['type'], $page->id);
 
         if (isset($data['name'])) {
             $image->refresh();
index 0e312d8832730b678e2c35502604966247037761..1e58816a4f3fc0b2da387896f78e558d1be87fde 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace BookStack\Uploads;
 
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Permissions\PermissionApplicator;
 use Exception;
@@ -15,6 +15,7 @@ class ImageRepo
         protected ImageService $imageService,
         protected PermissionApplicator $permissions,
         protected ImageResizer $imageResizer,
+        protected PageQueries $pageQueries,
     ) {
     }
 
@@ -77,14 +78,13 @@ class ImageRepo
      */
     public function getEntityFiltered(
         string $type,
-        string $filterType = null,
-        int $page = 0,
-        int $pageSize = 24,
-        int $uploadedTo = null,
-        string $search = null
+        ?string $filterType,
+        int $page,
+        int $pageSize,
+        int $uploadedTo,
+        ?string $search
     ): array {
-        /** @var Page $contextPage */
-        $contextPage = Page::visible()->findOrFail($uploadedTo);
+        $contextPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo);
         $parentFilter = null;
 
         if ($filterType === 'book' || $filterType === 'page') {
@@ -225,9 +225,9 @@ class ImageRepo
      */
     public function getPagesUsingImage(Image $image): array
     {
-        $pages = Page::visible()
+        $pages = $this->pageQueries->visibleForList()
             ->where('html', 'like', '%' . $image->url . '%')
-            ->get(['id', 'name', 'slug', 'book_id']);
+            ->get();
 
         foreach ($pages as $page) {
             $page->setAttribute('url', $page->getUrl());
index 1655a4cc3aa0d9c74a12f355a8cbb6880f011711..8d8da61ec185b42691fde6bd1673abeb44a307ad 100644 (file)
@@ -2,9 +2,7 @@
 
 namespace BookStack\Uploads;
 
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Exceptions\ImageUploadException;
 use Exception;
 use Illuminate\Support\Facades\DB;
@@ -20,6 +18,7 @@ class ImageService
     public function __construct(
         protected ImageStorage $storage,
         protected ImageResizer $resizer,
+        protected EntityQueries $queries,
     ) {
     }
 
@@ -278,15 +277,15 @@ class ImageService
         }
 
         if ($imageType === 'gallery' || $imageType === 'drawio') {
-            return Page::visible()->where('id', '=', $image->uploaded_to)->exists();
+            return $this->queries->pages->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
         }
 
         if ($imageType === 'cover_book') {
-            return Book::visible()->where('id', '=', $image->uploaded_to)->exists();
+            return $this->queries->books->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
         }
 
         if ($imageType === 'cover_bookshelf') {
-            return Bookshelf::visible()->where('id', '=', $image->uploaded_to)->exists();
+            return $this->queries->shelves->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
         }
 
         return false;
index bdf268260a1a7db0121ce0935c8cd03dd0684c69..963d69a624700efd2df0cf1cbd8e90e5fabc0f11 100644 (file)
@@ -10,16 +10,25 @@ use BookStack\Users\UserRepo;
 
 class UserProfileController extends Controller
 {
+    public function __construct(
+        protected UserRepo $userRepo,
+        protected ActivityQueries $activityQueries,
+        protected UserContentCounts $contentCounts,
+        protected UserRecentlyCreatedContent $recentlyCreatedContent
+    ) {
+    }
+
+
     /**
      * Show the user profile page.
      */
-    public function show(UserRepo $repo, ActivityQueries $activities, string $slug)
+    public function show(string $slug)
     {
-        $user = $repo->getBySlug($slug);
+        $user = $this->userRepo->getBySlug($slug);
 
-        $userActivity = $activities->userActivity($user);
-        $recentlyCreated = (new UserRecentlyCreatedContent())->run($user, 5);
-        $assetCounts = (new UserContentCounts())->run($user);
+        $userActivity = $this->activityQueries->userActivity($user);
+        $recentlyCreated = $this->recentlyCreatedContent->run($user, 5);
+        $assetCounts = $this->contentCounts->run($user);
 
         $this->setPageTitle($user->name);
 
index 178d8536b1d44441d4dc0ef0950e44947b712992..af38bfa7e2a2df6c880de67da030870535a9b26e 100644 (file)
@@ -2,10 +2,7 @@
 
 namespace BookStack\Users\Queries;
 
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Users\Models\User;
 
 /**
@@ -13,6 +10,12 @@ use BookStack\Users\Models\User;
  */
 class UserContentCounts
 {
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
+
+
     /**
      * @return array{pages: int, chapters: int, books: int, shelves: int}
      */
@@ -21,10 +24,10 @@ class UserContentCounts
         $createdBy = ['created_by' => $user->id];
 
         return [
-            'pages'    => Page::visible()->where($createdBy)->count(),
-            'chapters' => Chapter::visible()->where($createdBy)->count(),
-            'books'    => Book::visible()->where($createdBy)->count(),
-            'shelves'  => Bookshelf::visible()->where($createdBy)->count(),
+            'pages'    => $this->queries->pages->visibleForList()->where($createdBy)->count(),
+            'chapters' => $this->queries->chapters->visibleForList()->where($createdBy)->count(),
+            'books'    => $this->queries->books->visibleForList()->where($createdBy)->count(),
+            'shelves'  => $this->queries->shelves->visibleForList()->where($createdBy)->count(),
         ];
     }
 }
index 23db2c1f1a2a42ad38254b0237496f904a1537ec..23850e072010de48d164544fac92d0eeebb780c5 100644 (file)
@@ -2,10 +2,7 @@
 
 namespace BookStack\Users\Queries;
 
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Users\Models\User;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Collection;
@@ -15,6 +12,11 @@ use Illuminate\Database\Eloquent\Collection;
  */
 class UserRecentlyCreatedContent
 {
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
+
     /**
      * @return array{pages: Collection, chapters: Collection, books: Collection, shelves: Collection}
      */
@@ -28,10 +30,10 @@ class UserRecentlyCreatedContent
         };
 
         return [
-            'pages'    => $query(Page::visible()->where('draft', '=', false)),
-            'chapters' => $query(Chapter::visible()),
-            'books'    => $query(Book::visible()),
-            'shelves'  => $query(Bookshelf::visible()),
+            'pages'    => $query($this->queries->pages->visibleForList()->where('draft', '=', false)),
+            'chapters' => $query($this->queries->chapters->visibleForList()),
+            'books'    => $query($this->queries->books->visibleForList()),
+            'shelves'  => $query($this->queries->shelves->visibleForList()),
         ];
     }
 }
diff --git a/database/migrations/2024_02_04_141358_add_views_updated_index.php b/database/migrations/2024_02_04_141358_add_views_updated_index.php
new file mode 100644 (file)
index 0000000..a643b3a
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('views', function (Blueprint $table) {
+            $table->index(['updated_at'], 'views_updated_at_index');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('views', function (Blueprint $table) {
+            $table->dropIndex('views_updated_at_index');
+        });
+    }
+};
index 27d66b30bb9a7a71ad6ea34e235b1d17e1c75f2b..dc24a558d84f2d660f0452c299167e71da3f398b 100644 (file)
@@ -1,5 +1,5 @@
 @extends('layouts.simple')
-
+@inject('popular', \BookStack\Entities\Queries\QueryPopular::class)
 @section('content')
     <div class="container mt-l">
 
@@ -28,7 +28,7 @@
                     <div class="card mb-xl">
                         <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
                         <div class="px-m">
-                            @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
+                            @include('entities.list', ['entities' => $popular->run(10, 0, ['page']), 'style' => 'compact'])
                         </div>
                     </div>
                 </div>
@@ -36,7 +36,7 @@
                     <div class="card mb-xl">
                         <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
                         <div class="px-m">
-                            @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
+                            @include('entities.list', ['entities' => $popular->run(10, 0, ['book']), 'style' => 'compact'])
                         </div>
                     </div>
                 </div>
@@ -44,7 +44,7 @@
                     <div class="card mb-xl">
                         <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
                         <div class="px-m">
-                            @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
+                            @include('entities.list', ['entities' => $popular->run(10, 0, ['chapter']), 'style' => 'compact'])
                         </div>
                     </div>
                 </div>
index b31bd7d37ee06e66de9940a1a710bc5ab2eab7ec..b8c2b613387ef79a156ecc783e5e9dd067083bca 100644 (file)
@@ -24,6 +24,9 @@ class BooksApiTest extends TestCase
                 'id'   => $firstBook->id,
                 'name' => $firstBook->name,
                 'slug' => $firstBook->slug,
+                'owned_by' => $firstBook->owned_by,
+                'created_by' => $firstBook->created_by,
+                'updated_by' => $firstBook->updated_by,
             ],
         ]]);
     }
index e2d6cfc81f0a8ce5e12172c7f4c793787feff2e4..9698d4dd9c254dea3ba5d3e1a6390bd61033b84d 100644 (file)
@@ -28,6 +28,9 @@ class ChaptersApiTest extends TestCase
                 'book_id'   => $firstChapter->book->id,
                 'priority'  => $firstChapter->priority,
                 'book_slug' => $firstChapter->book->slug,
+                'owned_by'   => $firstChapter->owned_by,
+                'created_by' => $firstChapter->created_by,
+                'updated_by' => $firstChapter->updated_by,
             ],
         ]]);
     }
@@ -149,6 +152,16 @@ class ChaptersApiTest extends TestCase
                     'id'   => $page->id,
                     'slug' => $page->slug,
                     'name' => $page->name,
+                    'owned_by' => $page->owned_by,
+                    'created_by' => $page->created_by,
+                    'updated_by' => $page->updated_by,
+                    'book_id' => $page->id,
+                    'chapter_id' => $chapter->id,
+                    'priority' => $page->priority,
+                    'book_slug' => $chapter->book->slug,
+                    'draft' => $page->draft,
+                    'template' => $page->template,
+                    'editor' => $page->editor,
                 ],
             ],
             'default_template_id' => null,
index 0d084472d1a43e0caf78136a78b07801b8c816ed..22659d5bb726bf011403ce14cc728d810bde861a 100644 (file)
@@ -27,6 +27,10 @@ class PagesApiTest extends TestCase
                 'slug'     => $firstPage->slug,
                 'book_id'  => $firstPage->book->id,
                 'priority' => $firstPage->priority,
+                'owned_by'   => $firstPage->owned_by,
+                'created_by' => $firstPage->created_by,
+                'updated_by' => $firstPage->updated_by,
+                'revision_count' => $firstPage->revision_count,
             ],
         ]]);
     }
index f1b8ed98553cce61f099273f52dd7c1784d9511d..be276e110040cd156c68e7d996ee26b458a1373c 100644 (file)
@@ -25,6 +25,9 @@ class ShelvesApiTest extends TestCase
                 'id'   => $firstBookshelf->id,
                 'name' => $firstBookshelf->name,
                 'slug' => $firstBookshelf->slug,
+                'owned_by' => $firstBookshelf->owned_by,
+                'created_by' => $firstBookshelf->created_by,
+                'updated_by' => $firstBookshelf->updated_by,
             ],
         ]]);
     }
index 3740892460b296e44b7d4c88e17afb6c7709d421..04dff293facd20805e1281b4264f8808470c902e 100644 (file)
@@ -317,7 +317,7 @@ class BookTest extends TestCase
         $copy = Book::query()->where('name', '=', 'My copy book')->first();
 
         $resp->assertRedirect($copy->getUrl());
-        $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
+        $this->assertEquals($book->getDirectVisibleChildren()->count(), $copy->getDirectVisibleChildren()->count());
 
         $this->get($copy->getUrl())->assertSee($book->description_html, false);
     }
@@ -329,7 +329,7 @@ class BookTest extends TestCase
 
         // Hide child content
         /** @var BookChild $page */
-        foreach ($book->getDirectChildren() as $child) {
+        foreach ($book->getDirectVisibleChildren() as $child) {
             $this->permissions->setEntityPermissions($child, [], []);
         }
 
@@ -337,7 +337,7 @@ class BookTest extends TestCase
         /** @var Book $copy */
         $copy = Book::query()->where('name', '=', 'My copy book')->first();
 
-        $this->assertEquals(0, $copy->getDirectChildren()->count());
+        $this->assertEquals(0, $copy->getDirectVisibleChildren()->count());
     }
 
     public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
index 9b77a32ab80e9e63f180a80243230733b36b02df..dcc0620444628c92680666578db25e46831e28b6 100644 (file)
@@ -303,7 +303,7 @@ class EntitySearchTest extends TestCase
     public function test_sibling_search_for_pages_without_chapter()
     {
         $page = $this->entities->pageNotWithinChapter();
-        $bookChildren = $page->book->getDirectChildren();
+        $bookChildren = $page->book->getDirectVisibleChildren();
         $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
 
         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
@@ -318,7 +318,7 @@ class EntitySearchTest extends TestCase
     public function test_sibling_search_for_chapters()
     {
         $chapter = $this->entities->chapter();
-        $bookChildren = $chapter->book->getDirectChildren();
+        $bookChildren = $chapter->book->getDirectVisibleChildren();
         $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
 
         $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter");