0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-05-05 08:40:11 +00:00

Merge pull request from BookStackApp/query_revamp

Update of entity loading to be more efficient and avoid global addSelects
This commit is contained in:
Dan Brown 2024-02-11 15:56:32 +00:00 committed by GitHub
commit ff8daad22b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 1201 additions and 918 deletions
app
Activity
Api
App
Config
Console/Commands
Entities
Permissions
References
Search
Settings
Uploads
Users
database/migrations
resources/views/errors
tests

View file

@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -14,11 +15,10 @@ use Illuminate\Database\Eloquent\Relations\Relation;
class ActivityQueries class ActivityQueries
{ {
protected PermissionApplicator $permissions; public function __construct(
protected PermissionApplicator $permissions,
public function __construct(PermissionApplicator $permissions) protected MixedEntityListLoader $listLoader,
{ ) {
$this->permissions = $permissions;
} }
/** /**
@ -29,11 +29,13 @@ class ActivityQueries
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->with(['user', 'entity']) ->with(['user'])
->skip($count * $page) ->skip($count * $page)
->take($count) ->take($count)
->get(); ->get();
$this->listLoader->loadIntoRelations($activityList->all(), 'entity', false);
return $this->filterSimilar($activityList); return $this->filterSimilar($activityList);
} }

View file

@ -3,7 +3,7 @@
namespace BookStack\Activity\Controllers; namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo; use BookStack\Activity\CommentRepo;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -11,7 +11,8 @@ use Illuminate\Validation\ValidationException;
class CommentController extends Controller class CommentController extends Controller
{ {
public function __construct( public function __construct(
protected CommentRepo $commentRepo protected CommentRepo $commentRepo,
protected PageQueries $pageQueries,
) { ) {
} }
@ -27,7 +28,7 @@ class CommentController extends Controller
'parent_id' => ['nullable', 'integer'], 'parent_id' => ['nullable', 'integer'],
]); ]);
$page = Page::visible()->find($pageId); $page = $this->pageQueries->findVisibleById($pageId);
if ($page === null) { if ($page === null) {
return response('Not found', 404); return response('Not found', 404);
} }

View file

@ -2,7 +2,7 @@
namespace BookStack\Activity\Controllers; namespace BookStack\Activity\Controllers;
use BookStack\Entities\Queries\TopFavourites; use BookStack\Entities\Queries\QueryTopFavourites;
use BookStack\Entities\Tools\MixedEntityRequestHelper; use BookStack\Entities\Tools\MixedEntityRequestHelper;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -17,11 +17,11 @@ class FavouriteController extends Controller
/** /**
* Show a listing of all favourite items for the current user. * 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; $viewCount = 20;
$page = intval($request->get('page', 1)); $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; $hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;

View file

@ -7,7 +7,6 @@ use Exception;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;

View file

@ -3,12 +3,10 @@
namespace BookStack\App; namespace BookStack\App;
use BookStack\Activity\ActivityQueries; use BookStack\Activity\ActivityQueries;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\RecentlyViewed; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Queries\TopFavourites; use BookStack\Entities\Queries\QueryRecentlyViewed;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Queries\QueryTopFavourites;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageContent;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Uploads\FaviconHandler; use BookStack\Uploads\FaviconHandler;
@ -17,18 +15,25 @@ use Illuminate\Http\Request;
class HomeController extends Controller class HomeController extends Controller
{ {
public function __construct(
protected EntityQueries $queries,
) {
}
/** /**
* Display the homepage. * 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); $activity = $activities->latest(10);
$draftPages = []; $draftPages = [];
if ($this->isSignedIn()) { if ($this->isSignedIn()) {
$draftPages = Page::visible() $draftPages = $this->queries->pages->currentUserDraftsForList()
->where('draft', '=', true)
->where('created_by', '=', user()->id)
->orderBy('updated_at', 'desc') ->orderBy('updated_at', 'desc')
->with('book') ->with('book')
->take(6) ->take(6)
@ -37,14 +42,13 @@ class HomeController extends Controller
$recentFactor = count($draftPages) > 0 ? 0.5 : 1; $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ? $recents = $this->isSignedIn() ?
(new RecentlyViewed())->run(12 * $recentFactor, 1) $recentlyViewed->run(12 * $recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get(); : $this->queries->books->visibleForList()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
$favourites = (new TopFavourites())->run(6); $favourites = $topFavourites->run(6);
$recentlyUpdatedPages = Page::visible()->with('book') $recentlyUpdatedPages = $this->queries->pages->visibleForList()
->where('draft', false) ->where('draft', false)
->orderBy('updated_at', 'desc') ->orderBy('updated_at', 'desc')
->take($favourites->count() > 0 ? 5 : 10) ->take($favourites->count() > 0 ? 5 : 10)
->select(Page::$listAttributes)
->get(); ->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page']; $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@ -78,14 +82,18 @@ class HomeController extends Controller
} }
if ($homepageOption === 'bookshelves') { 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]); $data = array_merge($commonData, ['shelves' => $shelves]);
return view('home.shelves', $data); return view('home.shelves', $data);
} }
if ($homepageOption === 'books') { 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]); $data = array_merge($commonData, ['books' => $books]);
return view('home.books', $data); return view('home.books', $data);
@ -95,7 +103,7 @@ class HomeController extends Controller
$homepageSetting = setting('app-homepage', '0:'); $homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]); $id = intval(explode(':', $homepageSetting)[0]);
/** @var Page $customHomepage */ /** @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); $pageContent = new PageContent($customHomepage);
$customHomepage->html = $pageContent->render(false); $customHomepage->html = $pageContent->render(false);

View file

@ -4,7 +4,6 @@ namespace BookStack\App\Providers;
use BookStack\Theming\ThemeEvents; use BookStack\Theming\ThemeEvents;
use BookStack\Theming\ThemeService; use BookStack\Theming\ThemeService;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class ThemeServiceProvider extends ServiceProvider class ThemeServiceProvider extends ServiceProvider

View file

@ -173,6 +173,8 @@ return [
// List of URIs that should not be collected // List of URIs that should not be collected
'except' => [ 'except' => [
'/uploads/images/.*', // BookStack image requests
'/horizon/.*', // Laravel Horizon requests '/horizon/.*', // Laravel Horizon requests
'/telescope/.*', // Laravel Telescope requests '/telescope/.*', // Laravel Telescope requests
'/_debugbar/.*', // Laravel DebugBar requests '/_debugbar/.*', // Laravel DebugBar requests

View file

@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Entities\Tools\PermissionsUpdater;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -28,7 +28,7 @@ class CopyShelfPermissionsCommand extends Command
/** /**
* Execute the console command. * Execute the console command.
*/ */
public function handle(PermissionsUpdater $permissionsUpdater): int public function handle(PermissionsUpdater $permissionsUpdater, BookshelfQueries $queries): int
{ {
$shelfSlug = $this->option('slug'); $shelfSlug = $this->option('slug');
$cascadeAll = $this->option('all'); $cascadeAll = $this->option('all');
@ -51,11 +51,11 @@ class CopyShelfPermissionsCommand extends Command
return 0; return 0;
} }
$shelves = Bookshelf::query()->get(['id']); $shelves = $queries->start()->get(['id']);
} }
if ($shelfSlug) { if ($shelfSlug) {
$shelves = Bookshelf::query()->where('slug', '=', $shelfSlug)->get(['id']); $shelves = $queries->start()->where('slug', '=', $shelfSlug)->get(['id']);
if ($shelves->count() === 0) { if ($shelves->count() === 0) {
$this->info('No shelves found with the given slug.'); $this->info('No shelves found with the given slug.');
} }

View file

@ -6,6 +6,7 @@ use BookStack\Api\ApiEntityListFormatter;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
@ -15,7 +16,8 @@ use Illuminate\Validation\ValidationException;
class BookApiController extends ApiController class BookApiController extends ApiController
{ {
public function __construct( public function __construct(
protected BookRepo $bookRepo protected BookRepo $bookRepo,
protected BookQueries $queries,
) { ) {
} }
@ -24,7 +26,9 @@ class BookApiController extends ApiController
*/ */
public function list() public function list()
{ {
$books = Book::visible(); $books = $this->queries
->visibleForList()
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($books, [ return $this->apiListingResponse($books, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', '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) public function read(string $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail(intval($id));
$book = $this->forJsonDisplay($book); $book = $this->forJsonDisplay($book);
$book->load(['createdBy', 'updatedBy', 'ownedBy']); $book->load(['createdBy', 'updatedBy', 'ownedBy']);
@ -83,7 +87,7 @@ class BookApiController extends ApiController
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$requestData = $this->validate($request, $this->rules()['update']); $requestData = $this->validate($request, $this->rules()['update']);
@ -100,7 +104,7 @@ class BookApiController extends ApiController
*/ */
public function delete(string $id) public function delete(string $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book); $this->bookRepo->destroy($book);

View file

@ -6,7 +6,8 @@ use BookStack\Activity\ActivityQueries;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions; 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\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner; use BookStack\Entities\Tools\Cloner;
@ -27,7 +28,9 @@ class BookController extends Controller
public function __construct( public function __construct(
protected ShelfContext $shelfContext, protected ShelfContext $shelfContext,
protected BookRepo $bookRepo, 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'), 'updated_at' => trans('common.sort_updated_at'),
]); ]);
$books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder()); $books = $this->queries->visibleForListWithCover()
$recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false; ->orderBy($listOptions->getSort(), $listOptions->getOrder())
$popular = $this->bookRepo->getPopular(4); ->paginate(18);
$new = $this->bookRepo->getRecentlyCreated(4); $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(); $this->shelfContext->clearShelfContext();
@ -71,7 +76,7 @@ class BookController extends Controller
$bookshelf = null; $bookshelf = null;
if ($shelfSlug !== null) { if ($shelfSlug !== null) {
$bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail(); $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf); $this->checkOwnablePermission('bookshelf-update', $bookshelf);
} }
@ -101,7 +106,7 @@ class BookController extends Controller
$bookshelf = null; $bookshelf = null;
if ($shelfSlug !== null) { if ($shelfSlug !== null) {
$bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail(); $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf); $this->checkOwnablePermission('bookshelf-update', $bookshelf);
} }
@ -120,7 +125,7 @@ class BookController extends Controller
*/ */
public function show(Request $request, ActivityQueries $activities, string $slug) 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); $bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->scopes('visible')->get(); $bookParentShelves = $book->shelves()->scopes('visible')->get();
@ -147,7 +152,7 @@ class BookController extends Controller
*/ */
public function edit(string $slug) public function edit(string $slug)
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()])); $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) public function update(Request $request, string $slug)
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$validated = $this->validate($request, [ $validated = $this->validate($request, [
@ -190,7 +195,7 @@ class BookController extends Controller
*/ */
public function showDelete(string $bookSlug) public function showDelete(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()])); $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
@ -204,7 +209,7 @@ class BookController extends Controller
*/ */
public function destroy(string $bookSlug) public function destroy(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book); $this->bookRepo->destroy($book);
@ -219,7 +224,7 @@ class BookController extends Controller
*/ */
public function showCopy(string $bookSlug) public function showCopy(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-view', $book); $this->checkOwnablePermission('book-view', $book);
session()->flashInput(['name' => $book->name]); session()->flashInput(['name' => $book->name]);
@ -236,7 +241,7 @@ class BookController extends Controller
*/ */
public function copy(Request $request, Cloner $cloner, string $bookSlug) 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->checkOwnablePermission('book-view', $book);
$this->checkPermission('book-create-all'); $this->checkPermission('book-create-all');
@ -252,7 +257,7 @@ class BookController extends Controller
*/ */
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug) 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-update', $book);
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
$this->checkPermission('bookshelf-create-all'); $this->checkPermission('bookshelf-create-all');

View file

@ -2,18 +2,17 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use Throwable; use Throwable;
class BookExportApiController extends ApiController class BookExportApiController extends ApiController
{ {
protected $exportFormatter; public function __construct(
protected ExportFormatter $exportFormatter,
public function __construct(ExportFormatter $exportFormatter) protected BookQueries $queries,
{ ) {
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export'); $this->middleware('can:content-export');
} }
@ -24,7 +23,7 @@ class BookExportApiController extends ApiController
*/ */
public function exportPdf(int $id) public function exportPdf(int $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail($id);
$pdfContent = $this->exportFormatter->bookToPdf($book); $pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->download()->directly($pdfContent, $book->slug . '.pdf'); return $this->download()->directly($pdfContent, $book->slug . '.pdf');
@ -37,7 +36,7 @@ class BookExportApiController extends ApiController
*/ */
public function exportHtml(int $id) public function exportHtml(int $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail($id);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book); $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->download()->directly($htmlContent, $book->slug . '.html'); return $this->download()->directly($htmlContent, $book->slug . '.html');
@ -48,7 +47,7 @@ class BookExportApiController extends ApiController
*/ */
public function exportPlainText(int $id) public function exportPlainText(int $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail($id);
$textContent = $this->exportFormatter->bookToPlainText($book); $textContent = $this->exportFormatter->bookToPlainText($book);
return $this->download()->directly($textContent, $book->slug . '.txt'); return $this->download()->directly($textContent, $book->slug . '.txt');
@ -59,7 +58,7 @@ class BookExportApiController extends ApiController
*/ */
public function exportMarkdown(int $id) public function exportMarkdown(int $id)
{ {
$book = Book::visible()->findOrFail($id); $book = $this->queries->findVisibleByIdOrFail($id);
$markdown = $this->exportFormatter->bookToMarkdown($book); $markdown = $this->exportFormatter->bookToMarkdown($book);
return $this->download()->directly($markdown, $book->slug . '.md'); return $this->download()->directly($markdown, $book->slug . '.md');

View file

@ -2,23 +2,17 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use Throwable; use Throwable;
class BookExportController extends Controller class BookExportController extends Controller
{ {
protected $bookRepo; public function __construct(
protected $exportFormatter; protected BookQueries $queries,
protected ExportFormatter $exportFormatter,
/** ) {
* BookExportController constructor.
*/
public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
{
$this->bookRepo = $bookRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export'); $this->middleware('can:content-export');
} }
@ -29,7 +23,7 @@ class BookExportController extends Controller
*/ */
public function pdf(string $bookSlug) public function pdf(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$pdfContent = $this->exportFormatter->bookToPdf($book); $pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->download()->directly($pdfContent, $bookSlug . '.pdf'); return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
@ -42,7 +36,7 @@ class BookExportController extends Controller
*/ */
public function html(string $bookSlug) public function html(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book); $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->download()->directly($htmlContent, $bookSlug . '.html'); return $this->download()->directly($htmlContent, $bookSlug . '.html');
@ -53,7 +47,7 @@ class BookExportController extends Controller
*/ */
public function plainText(string $bookSlug) public function plainText(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$textContent = $this->exportFormatter->bookToPlainText($book); $textContent = $this->exportFormatter->bookToPlainText($book);
return $this->download()->directly($textContent, $bookSlug . '.txt'); return $this->download()->directly($textContent, $bookSlug . '.txt');
@ -64,7 +58,7 @@ class BookExportController extends Controller
*/ */
public function markdown(string $bookSlug) public function markdown(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$textContent = $this->exportFormatter->bookToMarkdown($book); $textContent = $this->exportFormatter->bookToMarkdown($book);
return $this->download()->directly($textContent, $bookSlug . '.md'); return $this->download()->directly($textContent, $bookSlug . '.md');

View file

@ -3,7 +3,7 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\BookSortMap; use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
@ -12,11 +12,9 @@ use Illuminate\Http\Request;
class BookSortController extends Controller class BookSortController extends Controller
{ {
protected $bookRepo; public function __construct(
protected BookQueries $queries,
public function __construct(BookRepo $bookRepo) ) {
{
$this->bookRepo = $bookRepo;
} }
/** /**
@ -24,7 +22,7 @@ class BookSortController extends Controller
*/ */
public function show(string $bookSlug) public function show(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$bookChildren = (new BookContents($book))->getTree(false); $bookChildren = (new BookContents($book))->getTree(false);
@ -40,7 +38,7 @@ class BookSortController extends Controller
*/ */
public function showItem(string $bookSlug) public function showItem(string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$bookChildren = (new BookContents($book))->getTree(); $bookChildren = (new BookContents($book))->getTree();
return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); 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) public function update(Request $request, string $bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
// Return if no map sent // Return if no map sent

View file

@ -3,6 +3,7 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use Exception; use Exception;
@ -13,7 +14,8 @@ use Illuminate\Validation\ValidationException;
class BookshelfApiController extends ApiController class BookshelfApiController extends ApiController
{ {
public function __construct( public function __construct(
protected BookshelfRepo $bookshelfRepo protected BookshelfRepo $bookshelfRepo,
protected BookshelfQueries $queries,
) { ) {
} }
@ -22,7 +24,9 @@ class BookshelfApiController extends ApiController
*/ */
public function list() public function list()
{ {
$shelves = Bookshelf::visible(); $shelves = $this->queries
->visibleForList()
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($shelves, [ return $this->apiListingResponse($shelves, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', '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) public function read(string $id)
{ {
$shelf = Bookshelf::visible()->findOrFail($id); $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$shelf = $this->forJsonDisplay($shelf); $shelf = $this->forJsonDisplay($shelf);
$shelf->load([ $shelf->load([
'createdBy', 'updatedBy', 'ownedBy', 'createdBy', 'updatedBy', 'ownedBy',
@ -78,7 +82,7 @@ class BookshelfApiController extends ApiController
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id)
{ {
$shelf = Bookshelf::visible()->findOrFail($id); $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('bookshelf-update', $shelf); $this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules()['update']); $requestData = $this->validate($request, $this->rules()['update']);
@ -97,7 +101,7 @@ class BookshelfApiController extends ApiController
*/ */
public function delete(string $id) public function delete(string $id)
{ {
$shelf = Bookshelf::visible()->findOrFail($id); $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('bookshelf-delete', $shelf); $this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf); $this->bookshelfRepo->destroy($shelf);

View file

@ -4,7 +4,8 @@ namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityQueries; use BookStack\Activity\ActivityQueries;
use BookStack\Activity\Models\View; 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\Repos\BookshelfRepo;
use BookStack\Entities\Tools\ShelfContext; use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
@ -20,8 +21,10 @@ class BookshelfController extends Controller
{ {
public function __construct( public function __construct(
protected BookshelfRepo $shelfRepo, protected BookshelfRepo $shelfRepo,
protected BookshelfQueries $queries,
protected BookQueries $bookQueries,
protected ShelfContext $shelfContext, protected ShelfContext $shelfContext,
protected ReferenceFetcher $referenceFetcher protected ReferenceFetcher $referenceFetcher,
) { ) {
} }
@ -37,10 +40,15 @@ class BookshelfController extends Controller
'updated_at' => trans('common.sort_updated_at'), 'updated_at' => trans('common.sort_updated_at'),
]); ]);
$shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder()); $shelves = $this->queries->visibleForListWithCover()
$recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false; ->orderBy($listOptions->getSort(), $listOptions->getOrder())
$popular = $this->shelfRepo->getPopular(4); ->paginate(18);
$new = $this->shelfRepo->getRecentlyCreated(4); $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->shelfContext->clearShelfContext();
$this->setPageTitle(trans('entities.shelves')); $this->setPageTitle(trans('entities.shelves'));
@ -61,7 +69,7 @@ class BookshelfController extends Controller
public function create() public function create()
{ {
$this->checkPermission('bookshelf-create-all'); $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')); $this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]); return view('shelves.create', ['books' => $books]);
@ -96,7 +104,7 @@ class BookshelfController extends Controller
*/ */
public function show(Request $request, ActivityQueries $activities, string $slug) public function show(Request $request, ActivityQueries $activities, string $slug)
{ {
$shelf = $this->shelfRepo->getBySlug($slug); $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-view', $shelf); $this->checkOwnablePermission('bookshelf-view', $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([ $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
@ -134,11 +142,14 @@ class BookshelfController extends Controller
*/ */
public function edit(string $slug) public function edit(string $slug)
{ {
$shelf = $this->shelfRepo->getBySlug($slug); $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf); $this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id'); $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()])); $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) public function update(Request $request, string $slug)
{ {
$shelf = $this->shelfRepo->getBySlug($slug); $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf); $this->checkOwnablePermission('bookshelf-update', $shelf);
$validated = $this->validate($request, [ $validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
@ -183,7 +194,7 @@ class BookshelfController extends Controller
*/ */
public function showDelete(string $slug) public function showDelete(string $slug)
{ {
$shelf = $this->shelfRepo->getBySlug($slug); $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf); $this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()])); $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
@ -198,7 +209,7 @@ class BookshelfController extends Controller
*/ */
public function destroy(string $slug) public function destroy(string $slug)
{ {
$shelf = $this->shelfRepo->getBySlug($slug); $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf); $this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->shelfRepo->destroy($shelf); $this->shelfRepo->destroy($shelf);

View file

@ -2,8 +2,9 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
@ -35,7 +36,9 @@ class ChapterApiController extends ApiController
]; ];
public function __construct( 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() public function list()
{ {
$chapters = Chapter::visible(); $chapters = $this->queries->visibleForList()
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($chapters, [ return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority', 'id', 'book_id', 'name', 'slug', 'description', 'priority',
@ -60,7 +64,7 @@ class ChapterApiController extends ApiController
$requestData = $this->validate($request, $this->rules['create']); $requestData = $this->validate($request, $this->rules['create']);
$bookId = $request->get('book_id'); $bookId = $request->get('book_id');
$book = Book::visible()->findOrFail($bookId); $book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
$this->checkOwnablePermission('chapter-create', $book); $this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($requestData, $book); $chapter = $this->chapterRepo->create($requestData, $book);
@ -73,15 +77,17 @@ class ChapterApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$chapter = $this->forJsonDisplay($chapter); $chapter = $this->forJsonDisplay($chapter);
$chapter->load([ $chapter->load(['createdBy', 'updatedBy', 'ownedBy']);
'createdBy', 'updatedBy', 'ownedBy',
'pages' => function (HasMany $query) { // Note: More fields than usual here, for backwards compatibility,
$query->scopes('visible')->get(['id', 'name', 'slug']); // 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); return response()->json($chapter);
} }
@ -94,7 +100,7 @@ class ChapterApiController extends ApiController
public function update(Request $request, string $id) public function update(Request $request, string $id)
{ {
$requestData = $this->validate($request, $this->rules()['update']); $requestData = $this->validate($request, $this->rules()['update']);
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) { 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) public function delete(string $id)
{ {
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('chapter-delete', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter); $this->chapterRepo->destroy($chapter);

View file

@ -5,6 +5,8 @@ namespace BookStack\Entities\Controllers;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner; use BookStack\Entities\Tools\Cloner;
@ -24,7 +26,9 @@ class ChapterController extends Controller
{ {
public function __construct( public function __construct(
protected ChapterRepo $chapterRepo, 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) public function create(string $bookSlug)
{ {
$book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail(); $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('chapter-create', $book); $this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create')); $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'], 'default_template_id' => ['nullable', 'integer'],
]); ]);
$book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail(); $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('chapter-create', $book); $this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($validated, $book); $chapter = $this->chapterRepo->create($validated, $book);
@ -68,11 +75,12 @@ class ChapterController extends Controller
*/ */
public function show(string $bookSlug, string $chapterSlug) public function show(string $bookSlug, string $chapterSlug)
{ {
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter); $this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = (new BookContents($chapter->book))->getTree(); $sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $chapter->getVisiblePages(); $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree); $nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
View::incrementFor($chapter); View::incrementFor($chapter);
@ -96,7 +104,7 @@ class ChapterController extends Controller
*/ */
public function edit(string $bookSlug, string $chapterSlug) 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->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()])); $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
@ -118,7 +126,7 @@ class ChapterController extends Controller
'default_template_id' => ['nullable', 'integer'], 'default_template_id' => ['nullable', 'integer'],
]); ]);
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
$this->chapterRepo->update($chapter, $validated); $this->chapterRepo->update($chapter, $validated);
@ -133,7 +141,7 @@ class ChapterController extends Controller
*/ */
public function showDelete(string $bookSlug, string $chapterSlug) 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->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()])); $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) 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->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter); $this->chapterRepo->destroy($chapter);
@ -164,7 +172,7 @@ class ChapterController extends Controller
*/ */
public function showMove(string $bookSlug, string $chapterSlug) 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->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter);
@ -182,7 +190,7 @@ class ChapterController extends Controller
*/ */
public function move(Request $request, string $bookSlug, string $chapterSlug) 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-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter);
@ -211,7 +219,7 @@ class ChapterController extends Controller
*/ */
public function showCopy(string $bookSlug, string $chapterSlug) public function showCopy(string $bookSlug, string $chapterSlug)
{ {
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter); $this->checkOwnablePermission('chapter-view', $chapter);
session()->flashInput(['name' => $chapter->name]); session()->flashInput(['name' => $chapter->name]);
@ -230,13 +238,13 @@ class ChapterController extends Controller
*/ */
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug) 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); $this->checkOwnablePermission('chapter-view', $chapter);
$entitySelection = $request->get('entity_selection') ?: null; $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')); $this->showErrorNotification(trans('errors.selected_book_not_found'));
return redirect($chapter->getUrl('/copy')); return redirect($chapter->getUrl('/copy'));
@ -256,7 +264,7 @@ class ChapterController extends Controller
*/ */
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug) 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-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all'); $this->checkPermission('book-create-all');

View file

@ -2,21 +2,17 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use Throwable; use Throwable;
class ChapterExportApiController extends ApiController class ChapterExportApiController extends ApiController
{ {
protected $exportFormatter; public function __construct(
protected ExportFormatter $exportFormatter,
/** protected ChapterQueries $queries,
* ChapterExportController constructor. ) {
*/
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export'); $this->middleware('can:content-export');
} }
@ -27,7 +23,7 @@ class ChapterExportApiController extends ApiController
*/ */
public function exportPdf(int $id) public function exportPdf(int $id)
{ {
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail($id);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter); $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->download()->directly($pdfContent, $chapter->slug . '.pdf'); return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
@ -40,7 +36,7 @@ class ChapterExportApiController extends ApiController
*/ */
public function exportHtml(int $id) public function exportHtml(int $id)
{ {
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail($id);
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter); $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->download()->directly($htmlContent, $chapter->slug . '.html'); return $this->download()->directly($htmlContent, $chapter->slug . '.html');
@ -51,7 +47,7 @@ class ChapterExportApiController extends ApiController
*/ */
public function exportPlainText(int $id) public function exportPlainText(int $id)
{ {
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail($id);
$textContent = $this->exportFormatter->chapterToPlainText($chapter); $textContent = $this->exportFormatter->chapterToPlainText($chapter);
return $this->download()->directly($textContent, $chapter->slug . '.txt'); return $this->download()->directly($textContent, $chapter->slug . '.txt');
@ -62,7 +58,7 @@ class ChapterExportApiController extends ApiController
*/ */
public function exportMarkdown(int $id) public function exportMarkdown(int $id)
{ {
$chapter = Chapter::visible()->findOrFail($id); $chapter = $this->queries->findVisibleByIdOrFail($id);
$markdown = $this->exportFormatter->chapterToMarkdown($chapter); $markdown = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->download()->directly($markdown, $chapter->slug . '.md'); return $this->download()->directly($markdown, $chapter->slug . '.md');

View file

@ -2,7 +2,7 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
@ -10,16 +10,10 @@ use Throwable;
class ChapterExportController extends Controller class ChapterExportController extends Controller
{ {
protected $chapterRepo; public function __construct(
protected $exportFormatter; protected ChapterQueries $queries,
protected ExportFormatter $exportFormatter,
/** ) {
* ChapterExportController constructor.
*/
public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
{
$this->chapterRepo = $chapterRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export'); $this->middleware('can:content-export');
} }
@ -31,7 +25,7 @@ class ChapterExportController extends Controller
*/ */
public function pdf(string $bookSlug, string $chapterSlug) public function pdf(string $bookSlug, string $chapterSlug)
{ {
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter); $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->download()->directly($pdfContent, $chapterSlug . '.pdf'); return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
@ -45,7 +39,7 @@ class ChapterExportController extends Controller
*/ */
public function html(string $bookSlug, string $chapterSlug) public function html(string $bookSlug, string $chapterSlug)
{ {
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter); $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->download()->directly($containedHtml, $chapterSlug . '.html'); return $this->download()->directly($containedHtml, $chapterSlug . '.html');
@ -58,7 +52,7 @@ class ChapterExportController extends Controller
*/ */
public function plainText(string $bookSlug, string $chapterSlug) public function plainText(string $bookSlug, string $chapterSlug)
{ {
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToPlainText($chapter); $chapterText = $this->exportFormatter->chapterToPlainText($chapter);
return $this->download()->directly($chapterText, $chapterSlug . '.txt'); return $this->download()->directly($chapterText, $chapterSlug . '.txt');
@ -71,7 +65,7 @@ class ChapterExportController extends Controller
*/ */
public function markdown(string $bookSlug, string $chapterSlug) public function markdown(string $bookSlug, string $chapterSlug)
{ {
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToMarkdown($chapter); $chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->download()->directly($chapterText, $chapterSlug . '.md'); return $this->download()->directly($chapterText, $chapterSlug . '.md');

View file

@ -2,9 +2,8 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
@ -35,7 +34,9 @@ class PageApiController extends ApiController
]; ];
public function __construct( 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() public function list()
{ {
$pages = Page::visible(); $pages = $this->queries->visibleForList()
->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
return $this->apiListingResponse($pages, [ return $this->apiListingResponse($pages, [
'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority', 'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
@ -70,9 +72,9 @@ class PageApiController extends ApiController
$this->validate($request, $this->rules['create']); $this->validate($request, $this->rules['create']);
if ($request->has('chapter_id')) { if ($request->has('chapter_id')) {
$parent = Chapter::visible()->findOrFail($request->get('chapter_id')); $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
} else { } else {
$parent = Book::visible()->findOrFail($request->get('book_id')); $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
} }
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
@ -97,7 +99,7 @@ class PageApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$page = $this->pageRepo->getById($id, []); $page = $this->queries->findVisibleByIdOrFail($id);
return response()->json($page->forJsonDisplay()); return response()->json($page->forJsonDisplay());
} }
@ -113,14 +115,14 @@ class PageApiController extends ApiController
{ {
$requestData = $this->validate($request, $this->rules['update']); $requestData = $this->validate($request, $this->rules['update']);
$page = $this->pageRepo->getById($id, []); $page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$parent = null; $parent = null;
if ($request->has('chapter_id')) { 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')) { } 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())) { if ($parent && !$parent->matches($page->getParent())) {
@ -148,7 +150,7 @@ class PageApiController extends ApiController
*/ */
public function delete(string $id) public function delete(string $id)
{ {
$page = $this->pageRepo->getById($id, []); $page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page); $this->pageRepo->destroy($page);

View file

@ -7,7 +7,8 @@ use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter; 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\Repos\PageRepo;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner; use BookStack\Entities\Tools\Cloner;
@ -29,6 +30,8 @@ class PageController extends Controller
{ {
public function __construct( public function __construct(
protected PageRepo $pageRepo, protected PageRepo $pageRepo,
protected PageQueries $queries,
protected EntityQueries $entityQueries,
protected ReferenceFetcher $referenceFetcher protected ReferenceFetcher $referenceFetcher
) { ) {
} }
@ -40,7 +43,12 @@ class PageController extends Controller
*/ */
public function create(string $bookSlug, string $chapterSlug = null) 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); $this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in // Redirect to draft edit screen if signed in
@ -67,7 +75,12 @@ class PageController extends Controller
'name' => ['required', 'string', 'max:255'], '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); $this->checkOwnablePermission('page-create', $parent);
$page = $this->pageRepo->getNewDraftPage($parent); $page = $this->pageRepo->getNewDraftPage($parent);
@ -85,10 +98,10 @@ class PageController extends Controller
*/ */
public function editDraft(Request $request, string $bookSlug, int $pageId) 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()); $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')); $this->setPageTitle(trans('entities.pages_edit_draft'));
return view('pages.edit', $editorData->getViewData()); return view('pages.edit', $editorData->getViewData());
@ -105,7 +118,7 @@ class PageController extends Controller
$this->validate($request, [ $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
]); ]);
$draftPage = $this->pageRepo->getById($pageId); $draftPage = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-create', $draftPage->getParent()); $this->checkOwnablePermission('page-create', $draftPage->getParent());
$page = $this->pageRepo->publishDraft($draftPage, $request->all()); $page = $this->pageRepo->publishDraft($draftPage, $request->all());
@ -122,11 +135,12 @@ class PageController extends Controller
public function show(string $bookSlug, string $pageSlug) public function show(string $bookSlug, string $pageSlug)
{ {
try { try {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
} catch (NotFoundException $e) { } 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; throw $e;
} }
@ -167,7 +181,7 @@ class PageController extends Controller
*/ */
public function getPageAjax(int $pageId) public function getPageAjax(int $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->queries->findVisibleByIdOrFail($pageId);
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown'])); $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
$page->makeHidden(['book']); $page->makeHidden(['book']);
@ -181,10 +195,10 @@ class PageController extends Controller
*/ */
public function edit(Request $request, string $bookSlug, string $pageSlug) 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); $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()) { if ($editorData->getWarnings()) {
$this->showWarningNotification(implode("\n", $editorData->getWarnings())); $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
} }
@ -205,7 +219,7 @@ class PageController extends Controller
$this->validate($request, [ $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
]); ]);
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->pageRepo->update($page, $request->all()); $this->pageRepo->update($page, $request->all());
@ -220,7 +234,7 @@ class PageController extends Controller
*/ */
public function saveDraft(Request $request, int $pageId) public function saveDraft(Request $request, int $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
if (!$this->isSignedIn()) { if (!$this->isSignedIn()) {
@ -245,7 +259,7 @@ class PageController extends Controller
*/ */
public function redirectFromLink(int $pageId) public function redirectFromLink(int $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->queries->findVisibleByIdOrFail($pageId);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -257,12 +271,12 @@ class PageController extends Controller
*/ */
public function showDelete(string $bookSlug, string $pageSlug) 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->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()])); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate = $usedAsTemplate =
Book::query()->where('default_template_id', '=', $page->id)->count() > 0 || $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0; $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
return view('pages.delete', [ return view('pages.delete', [
'book' => $page->book, 'book' => $page->book,
@ -279,12 +293,12 @@ class PageController extends Controller
*/ */
public function showDeleteDraft(string $bookSlug, int $pageId) public function showDeleteDraft(string $bookSlug, int $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()])); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate = $usedAsTemplate =
Book::query()->where('default_template_id', '=', $page->id)->count() > 0 || $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0; $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
return view('pages.delete', [ return view('pages.delete', [
'book' => $page->book, 'book' => $page->book,
@ -302,7 +316,7 @@ class PageController extends Controller
*/ */
public function destroy(string $bookSlug, string $pageSlug) public function destroy(string $bookSlug, string $pageSlug)
{ {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
$parent = $page->getParent(); $parent = $page->getParent();
@ -319,7 +333,7 @@ class PageController extends Controller
*/ */
public function destroyDraft(string $bookSlug, int $pageId) public function destroyDraft(string $bookSlug, int $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->queries->findVisibleByIdOrFail($pageId);
$book = $page->book; $book = $page->book;
$chapter = $page->chapter; $chapter = $page->chapter;
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
@ -344,7 +358,9 @@ class PageController extends Controller
$query->scopes('visible'); $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') ->orderBy('updated_at', 'desc')
->paginate(20) ->paginate(20)
->setPath(url('/pages/recently-updated')); ->setPath(url('/pages/recently-updated'));
@ -366,7 +382,7 @@ class PageController extends Controller
*/ */
public function showMove(string $bookSlug, string $pageSlug) 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-update', $page);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
@ -384,7 +400,7 @@ class PageController extends Controller
*/ */
public function move(Request $request, string $bookSlug, string $pageSlug) 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-update', $page);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
@ -413,7 +429,7 @@ class PageController extends Controller
*/ */
public function showCopy(string $bookSlug, string $pageSlug) public function showCopy(string $bookSlug, string $pageSlug)
{ {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]); session()->flashInput(['name' => $page->name]);
@ -431,13 +447,13 @@ class PageController extends Controller
*/ */
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug) 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); $this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection') ?: null; $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')); $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect($page->getUrl('/copy')); return redirect($page->getUrl('/copy'));

View file

@ -2,18 +2,17 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use Throwable; use Throwable;
class PageExportApiController extends ApiController class PageExportApiController extends ApiController
{ {
protected $exportFormatter; public function __construct(
protected ExportFormatter $exportFormatter,
public function __construct(ExportFormatter $exportFormatter) protected PageQueries $queries,
{ ) {
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export'); $this->middleware('can:content-export');
} }
@ -24,7 +23,7 @@ class PageExportApiController extends ApiController
*/ */
public function exportPdf(int $id) public function exportPdf(int $id)
{ {
$page = Page::visible()->findOrFail($id); $page = $this->queries->findVisibleByIdOrFail($id);
$pdfContent = $this->exportFormatter->pageToPdf($page); $pdfContent = $this->exportFormatter->pageToPdf($page);
return $this->download()->directly($pdfContent, $page->slug . '.pdf'); return $this->download()->directly($pdfContent, $page->slug . '.pdf');
@ -37,7 +36,7 @@ class PageExportApiController extends ApiController
*/ */
public function exportHtml(int $id) public function exportHtml(int $id)
{ {
$page = Page::visible()->findOrFail($id); $page = $this->queries->findVisibleByIdOrFail($id);
$htmlContent = $this->exportFormatter->pageToContainedHtml($page); $htmlContent = $this->exportFormatter->pageToContainedHtml($page);
return $this->download()->directly($htmlContent, $page->slug . '.html'); return $this->download()->directly($htmlContent, $page->slug . '.html');
@ -48,7 +47,7 @@ class PageExportApiController extends ApiController
*/ */
public function exportPlainText(int $id) public function exportPlainText(int $id)
{ {
$page = Page::visible()->findOrFail($id); $page = $this->queries->findVisibleByIdOrFail($id);
$textContent = $this->exportFormatter->pageToPlainText($page); $textContent = $this->exportFormatter->pageToPlainText($page);
return $this->download()->directly($textContent, $page->slug . '.txt'); return $this->download()->directly($textContent, $page->slug . '.txt');
@ -59,7 +58,7 @@ class PageExportApiController extends ApiController
*/ */
public function exportMarkdown(int $id) public function exportMarkdown(int $id)
{ {
$page = Page::visible()->findOrFail($id); $page = $this->queries->findVisibleByIdOrFail($id);
$markdown = $this->exportFormatter->pageToMarkdown($page); $markdown = $this->exportFormatter->pageToMarkdown($page);
return $this->download()->directly($markdown, $page->slug . '.md'); return $this->download()->directly($markdown, $page->slug . '.md');

View file

@ -2,7 +2,7 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
@ -11,16 +11,10 @@ use Throwable;
class PageExportController extends Controller class PageExportController extends Controller
{ {
protected $pageRepo; public function __construct(
protected $exportFormatter; protected PageQueries $queries,
protected ExportFormatter $exportFormatter,
/** ) {
* PageExportController constructor.
*/
public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter)
{
$this->pageRepo = $pageRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export'); $this->middleware('can:content-export');
} }
@ -33,7 +27,7 @@ class PageExportController extends Controller
*/ */
public function pdf(string $bookSlug, string $pageSlug) 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(); $page->html = (new PageContent($page))->render();
$pdfContent = $this->exportFormatter->pageToPdf($page); $pdfContent = $this->exportFormatter->pageToPdf($page);
@ -48,7 +42,7 @@ class PageExportController extends Controller
*/ */
public function html(string $bookSlug, string $pageSlug) 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(); $page->html = (new PageContent($page))->render();
$containedHtml = $this->exportFormatter->pageToContainedHtml($page); $containedHtml = $this->exportFormatter->pageToContainedHtml($page);
@ -62,7 +56,7 @@ class PageExportController extends Controller
*/ */
public function plainText(string $bookSlug, string $pageSlug) public function plainText(string $bookSlug, string $pageSlug)
{ {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToPlainText($page); $pageText = $this->exportFormatter->pageToPlainText($page);
return $this->download()->directly($pageText, $pageSlug . '.txt'); return $this->download()->directly($pageText, $pageSlug . '.txt');
@ -75,7 +69,7 @@ class PageExportController extends Controller
*/ */
public function markdown(string $bookSlug, string $pageSlug) public function markdown(string $bookSlug, string $pageSlug)
{ {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToMarkdown($page); $pageText = $this->exportFormatter->pageToMarkdown($page);
return $this->download()->directly($pageText, $pageSlug . '.md'); return $this->download()->directly($pageText, $pageSlug . '.md');

View file

@ -4,6 +4,7 @@ namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\PageRevision; use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Repos\RevisionRepo; use BookStack\Entities\Repos\RevisionRepo;
use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageContent;
@ -18,6 +19,7 @@ class PageRevisionController extends Controller
{ {
public function __construct( public function __construct(
protected PageRepo $pageRepo, protected PageRepo $pageRepo,
protected PageQueries $pageQueries,
protected RevisionRepo $revisionRepo, protected RevisionRepo $revisionRepo,
) { ) {
} }
@ -29,7 +31,7 @@ class PageRevisionController extends Controller
*/ */
public function index(Request $request, string $bookSlug, string $pageSlug) 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([ $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
'id' => trans('entities.pages_revisions_sort_number') 'id' => trans('entities.pages_revisions_sort_number')
]); ]);
@ -60,7 +62,7 @@ class PageRevisionController extends Controller
*/ */
public function show(string $bookSlug, string $pageSlug, int $revisionId) public function show(string $bookSlug, string $pageSlug, int $revisionId)
{ {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */ /** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first(); $revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) { if ($revision === null) {
@ -89,7 +91,7 @@ class PageRevisionController extends Controller
*/ */
public function changes(string $bookSlug, string $pageSlug, int $revisionId) public function changes(string $bookSlug, string $pageSlug, int $revisionId)
{ {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */ /** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first(); $revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) { if ($revision === null) {
@ -121,7 +123,7 @@ class PageRevisionController extends Controller
*/ */
public function restore(string $bookSlug, string $pageSlug, int $revisionId) 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); $this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $revisionId); $page = $this->pageRepo->restoreRevision($page, $revisionId);
@ -136,7 +138,7 @@ class PageRevisionController extends Controller
*/ */
public function destroy(string $bookSlug, string $pageSlug, int $revId) 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); $this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first(); $revision = $page->revisions()->where('id', '=', $revId)->first();
@ -162,7 +164,7 @@ class PageRevisionController extends Controller
*/ */
public function destroyUserDraft(string $pageId) public function destroyUserDraft(string $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->revisionRepo->deleteDraftsForCurrentUser($page); $this->revisionRepo->deleteDraftsForCurrentUser($page);
return response('', 200); return response('', 200);

View file

@ -2,6 +2,7 @@
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller; use BookStack\Http\Controller;
@ -9,14 +10,10 @@ use Illuminate\Http\Request;
class PageTemplateController extends Controller class PageTemplateController extends Controller
{ {
protected $pageRepo; public function __construct(
protected PageRepo $pageRepo,
/** protected PageQueries $pageQueries,
* PageTemplateController constructor. ) {
*/
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
} }
/** /**
@ -26,7 +23,19 @@ class PageTemplateController extends Controller
{ {
$page = $request->get('page', 1); $page = $request->get('page', 1);
$search = $request->get('search', ''); $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) { if ($search) {
$templates->appends(['search' => $search]); $templates->appends(['search' => $search]);
@ -44,7 +53,7 @@ class PageTemplateController extends Controller
*/ */
public function get(int $templateId) public function get(int $templateId)
{ {
$page = $this->pageRepo->getById($templateId); $page = $this->pageQueries->findVisibleByIdOrFail($templateId);
if (!$page->template) { if (!$page->template) {
throw new NotFoundException(); throw new NotFoundException();

View file

@ -116,9 +116,9 @@ class RecycleBinController extends Controller
* *
* @throws \Exception * @throws \Exception
*/ */
public function empty() public function empty(TrashCan $trash)
{ {
$deleteCount = (new TrashCan())->empty(); $deleteCount = $trash->empty();
$this->logActivity(ActivityType::RECYCLE_BIN_EMPTY); $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));

View file

@ -117,20 +117,11 @@ class Book extends Entity implements HasCoverImage
/** /**
* Get the direct child items within this book. * Get the direct child items within this book.
*/ */
public function getDirectChildren(): Collection public function getDirectVisibleChildren(): Collection
{ {
$pages = $this->directPages()->scopes('visible')->get(); $pages = $this->directPages()->scopes('visible')->get();
$chapters = $this->chapters()->scopes('visible')->get(); $chapters = $this->chapters()->scopes('visible')->get();
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft'); 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();
}
} }

View file

@ -13,38 +13,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $priority * @property int $priority
* @property string $book_slug * @property string $book_slug
* @property Book $book * @property Book $book
*
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
*/ */
abstract class BookChild extends Entity 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. * Get the book this page sits in.
*/ */

View file

@ -11,7 +11,6 @@ use Illuminate\Support\Collection;
* Class Chapter. * Class Chapter.
* *
* @property Collection<Page> $pages * @property Collection<Page> $pages
* @property string $description
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?Page $defaultTemplate * @property ?Page $defaultTemplate
*/ */
@ -70,13 +69,4 @@ class Chapter extends BookChild
->orderBy('priority', 'asc') ->orderBy('priority', 'asc')
->get(); ->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();
}
} }

View file

@ -32,9 +32,6 @@ class Page extends BookChild
{ {
use HasFactory; 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']; protected $fillable = ['name', 'priority'];
public string $textField = 'text'; public string $textField = 'text';
@ -145,13 +142,4 @@ class Page extends BookChild
return $refreshed; 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();
}
} }

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
}]));
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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');
}]);
}
}

View file

@ -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');
}
}

View file

@ -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;
}

View file

@ -3,24 +3,32 @@
namespace BookStack\Entities\Queries; namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; 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) public function run(int $count, int $page, array $filterModels = null)
{ {
$query = $this->permissionService() $query = $this->permissions
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type') ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count')) ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type') ->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc'); ->orderBy('view_count', 'desc');
if ($filterModels) { if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels)); $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
} }
$entities = $query->with('viewable') $entities = $query->with('viewable')
@ -35,7 +43,7 @@ class Popular extends EntityQuery
return $entities; return $entities;
} }
protected function loadBooksForChildren(Collection $entities) protected function loadBooksForChildren(Collection $entities): void
{ {
$bookChildren = $entities->filter(fn(Entity $entity) => $entity instanceof BookChild); $bookChildren = $entities->filter(fn(Entity $entity) => $entity instanceof BookChild);
$eloquent = (new \Illuminate\Database\Eloquent\Collection($bookChildren)); $eloquent = (new \Illuminate\Database\Eloquent\Collection($bookChildren));

View file

@ -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();
}
}

View file

@ -3,10 +3,18 @@
namespace BookStack\Entities\Queries; namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\Favourite; use BookStack\Activity\Models\Favourite;
use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Query\JoinClause; 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) public function run(int $count, int $skip = 0)
{ {
$user = user(); $user = user();
@ -14,7 +22,7 @@ class TopFavourites extends EntityQuery
return collect(); return collect();
} }
$query = $this->permissionService() $query = $this->permissions
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type') ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
->select('favourites.*') ->select('favourites.*')
->leftJoin('views', function (JoinClause $join) { ->leftJoin('views', function (JoinClause $join) {
@ -25,11 +33,13 @@ class TopFavourites extends EntityQuery
->orderBy('views.views', 'desc') ->orderBy('views.views', 'desc')
->where('favourites.user_id', '=', user()->id); ->where('favourites.user_id', '=', user()->id);
return $query->with('favouritable') $favourites = $query
->skip($skip) ->skip($skip)
->take($count) ->take($count)
->get() ->get();
->pluck('favouritable')
->filter(); $this->listLoader->loadIntoRelations($favourites->all(), 'favouritable', false);
return $favourites->pluck('favouritable')->filter();
} }
} }

View file

@ -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();
}
}

View file

@ -8,7 +8,7 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\HasHtmlDescription; use BookStack\Entities\Models\HasHtmlDescription;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\References\ReferenceStore; use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater; use BookStack\References\ReferenceUpdater;
@ -23,6 +23,7 @@ class BaseRepo
protected ImageRepo $imageRepo, protected ImageRepo $imageRepo,
protected ReferenceUpdater $referenceUpdater, protected ReferenceUpdater $referenceUpdater,
protected ReferenceStore $referenceStore, protected ReferenceStore $referenceStore,
protected PageQueries $pageQueries,
) { ) {
} }
@ -125,8 +126,7 @@ class BaseRepo
return; return;
} }
$templateExists = Page::query()->visible() $templateExists = $this->pageQueries->visibleTemplates()
->where('template', '=', true)
->where('id', '=', $templateId) ->where('id', '=', $templateId)
->exists(); ->exists();

View file

@ -5,79 +5,23 @@ namespace BookStack\Entities\Repos;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Activity\TagRepo; use BookStack\Activity\TagRepo;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageRepo;
use Exception; use Exception;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
class BookRepo class BookRepo
{ {
public function __construct( public function __construct(
protected BaseRepo $baseRepo, protected BaseRepo $baseRepo,
protected TagRepo $tagRepo, 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. * Create a new book in the system.
*/ */
@ -130,10 +74,9 @@ class BookRepo
*/ */
public function destroy(Book $book) public function destroy(Book $book)
{ {
$trashCan = new TrashCan(); $this->trashCan->softDestroyBook($book);
$trashCan->softDestroyBook($book);
Activity::add(ActivityType::BOOK_DELETE, $book); Activity::add(ActivityType::BOOK_DELETE, $book);
$trashCan->autoClearOld(); $this->trashCan->autoClearOld();
} }
} }

View file

@ -3,81 +3,19 @@
namespace BookStack\Entities\Repos; namespace BookStack\Entities\Repos;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use Exception; use Exception;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
class BookshelfRepo class BookshelfRepo
{ {
protected $baseRepo; public function __construct(
protected BaseRepo $baseRepo,
/** protected BookQueries $bookQueries,
* BookshelfRepo constructor. protected TrashCan $trashCan,
*/ ) {
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;
} }
/** /**
@ -124,7 +62,7 @@ class BookshelfRepo
return intval($id); return intval($id);
}); });
$syncData = Book::visible() $syncData = $this->bookQueries->visibleForList()
->whereIn('id', $bookIds) ->whereIn('id', $bookIds)
->pluck('id') ->pluck('id')
->mapWithKeys(function ($bookId) use ($numericIDs) { ->mapWithKeys(function ($bookId) use ($numericIDs) {
@ -141,9 +79,8 @@ class BookshelfRepo
*/ */
public function destroy(Bookshelf $shelf) public function destroy(Bookshelf $shelf)
{ {
$trashCan = new TrashCan(); $this->trashCan->softDestroyShelf($shelf);
$trashCan->softDestroyShelf($shelf);
Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf); Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf);
$trashCan->autoClearOld(); $this->trashCan->autoClearOld();
} }
} }

View file

@ -4,12 +4,11 @@ namespace BookStack\Entities\Repos;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use Exception; use Exception;
@ -17,26 +16,12 @@ use Exception;
class ChapterRepo class ChapterRepo
{ {
public function __construct( 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. * Create a new chapter in the system.
*/ */
@ -75,10 +60,9 @@ class ChapterRepo
*/ */
public function destroy(Chapter $chapter) public function destroy(Chapter $chapter)
{ {
$trashCan = new TrashCan(); $this->trashCan->softDestroyChapter($chapter);
$trashCan->softDestroyChapter($chapter);
Activity::add(ActivityType::CHAPTER_DELETE, $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 public function move(Chapter $chapter, string $parentIdentifier): Book
{ {
$parent = $this->findParentByIdentifier($parentIdentifier); $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);
if (is_null($parent)) { if (!$parent instanceof Book) {
throw new MoveOperationException('Book to move chapter into not found'); throw new MoveOperationException('Book to move chapter into not found');
} }
@ -106,24 +90,4 @@ class ChapterRepo
return $parent; 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();
}
} }

View file

@ -8,114 +8,30 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision; use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditorData; use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\References\ReferenceStore; use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater; use BookStack\References\ReferenceUpdater;
use Exception; use Exception;
use Illuminate\Pagination\LengthAwarePaginator;
class PageRepo class PageRepo
{ {
public function __construct( public function __construct(
protected BaseRepo $baseRepo, protected BaseRepo $baseRepo,
protected RevisionRepo $revisionRepo, protected RevisionRepo $revisionRepo,
protected EntityQueries $entityQueries,
protected ReferenceStore $referenceStore, 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. * Get a new draft page belonging to the given parent entity.
*/ */
@ -269,10 +185,9 @@ class PageRepo
*/ */
public function destroy(Page $page) public function destroy(Page $page)
{ {
$trashCan = new TrashCan(); $this->trashCan->softDestroyPage($page);
$trashCan->softDestroyPage($page);
Activity::add(ActivityType::PAGE_DELETE, $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 public function move(Page $page, string $parentIdentifier): Entity
{ {
$parent = $this->findParentByIdentifier($parentIdentifier); $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);
if (is_null($parent)) { if (!$parent instanceof Chapter && !$parent instanceof Book) {
throw new MoveOperationException('Book or chapter to move page into not found'); throw new MoveOperationException('Book or chapter to move page into not found');
} }
@ -343,28 +258,6 @@ class PageRepo
return $parent; 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. * Get a new priority for a page.
*/ */

View file

@ -4,39 +4,13 @@ namespace BookStack\Entities\Repos;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision; use BookStack\Entities\Models\PageRevision;
use Illuminate\Database\Eloquent\Builder; use BookStack\Entities\Queries\PageRevisionQueries;
class RevisionRepo class RevisionRepo
{ {
/** public function __construct(
* Get a revision by its stored book and page slug values. protected PageRevisionQueries $queries,
*/ ) {
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;
} }
/** /**
@ -44,7 +18,7 @@ class RevisionRepo
*/ */
public function deleteDraftsForCurrentUser(Page $page): void 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 public function getNewDraftForCurrentUser(Page $page): PageRevision
{ {
$draft = $this->getLatestDraftForCurrentUser($page); $draft = $this->queries->findLatestCurrentUserDraftsForPageId($page->id);
if ($draft) { if ($draft) {
return $draft; return $draft;
@ -116,16 +90,4 @@ class RevisionRepo
PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete(); 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');
}
} }

View file

@ -7,15 +7,17 @@ use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class BookContents class BookContents
{ {
protected Book $book; protected EntityQueries $queries;
public function __construct(Book $book) public function __construct(
{ protected Book $book,
$this->book = $book; ) {
$this->queries = app()->make(EntityQueries::class);
} }
/** /**
@ -23,10 +25,12 @@ class BookContents
*/ */
public function getLastPriority(): int public function getLastPriority(): int
{ {
$maxPage = Page::visible()->where('book_id', '=', $this->book->id) $maxPage = $this->book->pages()
->where('draft', '=', false) ->where('draft', '=', false)
->where('chapter_id', '=', 0)->max('priority'); ->where('chapter_id', '=', 0)
$maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id) ->max('priority');
$maxChapter = $this->book->chapters()
->max('priority'); ->max('priority');
return max($maxChapter, $maxPage, 1); return max($maxChapter, $maxPage, 1);
@ -38,7 +42,7 @@ class BookContents
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
{ {
$pages = $this->getPages($showDrafts, $renderPages); $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); $all = collect()->concat($pages)->concat($chapters);
$chapterMap = $chapters->keyBy('id'); $chapterMap = $chapters->keyBy('id');
$lonePages = collect(); $lonePages = collect();
@ -87,15 +91,17 @@ class BookContents
*/ */
protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
{ {
$query = Page::visible() if ($getPageContent) {
->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes) $query = $this->queries->pages->visibleWithContents();
->where('book_id', '=', $this->book->id); } else {
$query = $this->queries->pages->visibleForList();
}
if (!$showDrafts) { if (!$showDrafts) {
$query->where('draft', '=', false); $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 */ /** @var Book[] $booksInvolved */
$booksInvolved = array_values(array_filter($modelMap, function (string $key) { $booksInvolved = array_values(array_filter($modelMap, function (string $key) {
return strpos($key, 'book:') === 0; return str_starts_with($key, 'book:');
}, ARRAY_FILTER_USE_KEY)); }, ARRAY_FILTER_USE_KEY));
// Update permissions of books involved // 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 */ /** @var Page $page */
foreach ($pages as $page) { foreach ($pages as $page) {
$modelMap['page:' . $page->id] = $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 */ /** @var Chapter $chapter */
foreach ($chapters as $chapter) { foreach ($chapters as $chapter) {
$modelMap['chapter:' . $chapter->id] = $chapter; $modelMap['chapter:' . $chapter->id] = $chapter;
$ids['book'][] = $chapter->book_id; $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 */ /** @var Book $book */
foreach ($books as $book) { foreach ($books as $book) {
$modelMap['book:' . $book->id] = $book; $modelMap['book:' . $book->id] = $book;

View file

@ -77,7 +77,7 @@ class Cloner
$copyBook = $this->bookRepo->create($bookDetails); $copyBook = $this->bookRepo->create($bookDetails);
// Clone contents // Clone contents
$directChildren = $original->getDirectChildren(); $directChildren = $original->getDirectVisibleChildren();
foreach ($directChildren as $child) { foreach ($directChildren as $child) {
if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) { if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) {
$this->cloneChapter($child, $copyBook, $child->name); $this->cloneChapter($child, $copyBook, $child->name);

View file

@ -3,20 +3,13 @@
namespace BookStack\Entities\Tools; namespace BookStack\Entities\Tools;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\EntityProvider; use BookStack\Entities\Queries\EntityQueries;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
class MixedEntityListLoader 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( 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'. * This will look for a model id and type via 'name_id' and 'name_type'.
* @param Model[] $relations * @param Model[] $relations
*/ */
public function loadIntoRelations(array $relations, string $relationName): void public function loadIntoRelations(array $relations, string $relationName, bool $loadParents): void
{ {
$idsByType = []; $idsByType = [];
foreach ($relations as $relation) { foreach ($relations as $relation) {
@ -40,7 +33,7 @@ class MixedEntityListLoader
$idsByType[$type][] = $id; $idsByType[$type][] = $id;
} }
$modelMap = $this->idsByTypeToModelMap($idsByType); $modelMap = $this->idsByTypeToModelMap($idsByType, $loadParents);
foreach ($relations as $relation) { foreach ($relations as $relation) {
$type = $relation->getAttribute($relationName . '_type'); $type = $relation->getAttribute($relationName . '_type');
@ -56,21 +49,14 @@ class MixedEntityListLoader
* @param array<string, int[]> $idsByType * @param array<string, int[]> $idsByType
* @return array<string, array<int, Model>> * @return array<string, array<int, Model>>
*/ */
protected function idsByTypeToModelMap(array $idsByType): array protected function idsByTypeToModelMap(array $idsByType, bool $eagerLoadParents): array
{ {
$modelMap = []; $modelMap = [];
foreach ($idsByType as $type => $ids) { foreach ($idsByType as $type => $ids) {
if (!isset($this->listAttributes[$type])) { $models = $this->queries->visibleForList($type)
continue;
}
$instance = $this->entityProvider->get($type);
$models = $instance->newQuery()
->select($this->listAttributes[$type])
->scopes('visible')
->whereIn('id', $ids) ->whereIn('id', $ids)
->with($this->getRelationsToEagerLoad($type)) ->with($eagerLoadParents ? $this->getRelationsToEagerLoad($type) : [])
->get(); ->get();
if (count($models) > 0) { if (count($models) > 0) {

View file

@ -3,6 +3,7 @@
namespace BookStack\Entities\Tools; namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml; use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme; use BookStack\Facades\Theme;
@ -21,9 +22,12 @@ use Illuminate\Support\Str;
class PageContent class PageContent
{ {
protected PageQueries $pageQueries;
public function __construct( public function __construct(
protected Page $page protected Page $page
) { ) {
$this->pageQueries = app()->make(PageQueries::class);
} }
/** /**
@ -325,13 +329,14 @@ class PageContent
protected function getContentProviderClosure(bool $blankIncludes): Closure protected function getContentProviderClosure(bool $blankIncludes): Closure
{ {
$contextPage = $this->page; $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) { if ($blankIncludes) {
return PageIncludeContent::fromHtmlAndTag('', $tag); return PageIncludeContent::fromHtmlAndTag('', $tag);
} }
$matchedPage = Page::visible()->find($tag->getPageId()); $matchedPage = $queries->findVisibleById($tag->getPageId());
$content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag); $content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag);
if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) { if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) {

View file

@ -4,7 +4,7 @@ namespace BookStack\Entities\Tools;
use BookStack\Activity\Tools\CommentTree; use BookStack\Activity\Tools\CommentTree;
use BookStack\Entities\Models\Page; 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\HtmlToMarkdown;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml; use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
@ -15,7 +15,7 @@ class PageEditorData
public function __construct( public function __construct(
protected Page $page, protected Page $page,
protected PageRepo $pageRepo, protected EntityQueries $queries,
protected string $requestedEditor protected string $requestedEditor
) { ) {
$this->viewData = $this->build(); $this->viewData = $this->build();
@ -35,7 +35,12 @@ class PageEditorData
{ {
$page = clone $this->page; $page = clone $this->page;
$isDraft = boolval($this->page->draft); $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(); $draftsEnabled = auth()->check();
$isDraftRevision = false; $isDraftRevision = false;
@ -47,8 +52,8 @@ class PageEditorData
} }
// Check for a current draft version for this user // Check for a current draft version for this user
$userDraft = $this->pageRepo->getUserDraft($page); $userDraft = $this->queries->revisions->findLatestCurrentUserDraftsForPageId($page->id);
if ($userDraft !== null) { if (!is_null($userDraft)) {
$page->forceFill($userDraft->only(['name', 'html', 'markdown'])); $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$isDraftRevision = true; $isDraftRevision = true;
$this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft); $this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);

View file

@ -4,10 +4,16 @@ namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookshelfQueries;
class ShelfContext 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. * Get the current bookshelf context for the given book.
@ -20,8 +26,7 @@ class ShelfContext
return null; return null;
} }
/** @var Bookshelf $shelf */ $shelf = $this->shelfQueries->findVisibleById($contextBookshelfId);
$shelf = Bookshelf::visible()->find($contextBookshelfId);
$shelfContainsBook = $shelf && $shelf->contains($book); $shelfContainsBook = $shelf && $shelf->contains($book);
return $shelfContainsBook ? $shelf : null; return $shelfContainsBook ? $shelf : null;
@ -30,7 +35,7 @@ class ShelfContext
/** /**
* Store the current contextual shelf ID. * 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); session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
} }
@ -38,7 +43,7 @@ class ShelfContext
/** /**
* Clear the session stored shelf context id. * Clear the session stored shelf context id.
*/ */
public function clearShelfContext() public function clearShelfContext(): void
{ {
session()->forget($this->KEY_SHELF_CONTEXT_ID); session()->forget($this->KEY_SHELF_CONTEXT_ID);
} }

View file

@ -7,10 +7,17 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class SiblingFetcher class SiblingFetcher
{ {
public function __construct(
protected EntityQueries $queries,
protected ShelfContext $shelfContext,
) {
}
/** /**
* Search among the siblings of the entity of given type and id. * Search among the siblings of the entity of given type and id.
*/ */
@ -26,23 +33,23 @@ class SiblingFetcher
// Page in book or chapter // Page in book or chapter
if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) { if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) {
$entities = $entity->book->getDirectChildren(); $entities = $entity->book->getDirectVisibleChildren();
} }
// Book // Book
// Gets just the books in a shelf if shelf is in context // Gets just the books in a shelf if shelf is in context
if ($entity instanceof Book) { if ($entity instanceof Book) {
$contextShelf = (new ShelfContext())->getContextualShelfForBook($entity); $contextShelf = $this->shelfContext->getContextualShelfForBook($entity);
if ($contextShelf) { if ($contextShelf) {
$entities = $contextShelf->visibleBooks()->get(); $entities = $contextShelf->visibleBooks()->get();
} else { } else {
$entities = Book::visible()->get(); $entities = $this->queries->books->visibleForList()->get();
} }
} }
// Shelf // Shelf
if ($entity instanceof Bookshelf) { if ($entity instanceof Bookshelf) {
$entities = Bookshelf::visible()->get(); $entities = $this->queries->shelves->visibleForList()->get();
} }
return $entities; return $entities;

View file

@ -10,6 +10,7 @@ use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Uploads\AttachmentService; use BookStack\Uploads\AttachmentService;
@ -20,6 +21,11 @@ use Illuminate\Support\Carbon;
class TrashCan class TrashCan
{ {
public function __construct(
protected EntityQueries $queries,
) {
}
/** /**
* Send a shelf to the recycle bin. * Send a shelf to the recycle bin.
* *
@ -203,11 +209,13 @@ class TrashCan
} }
// Remove book template usages // 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]); ->update(['default_template_id' => null]);
// Remove chapter template usages // 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]); ->update(['default_template_id' => null]);
$page->forceDelete(); $page->forceDelete();

View file

@ -4,10 +4,10 @@ namespace BookStack\Permissions;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use BookStack\Users\Models\Role; use BookStack\Users\Models\Role;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -20,6 +20,12 @@ use Illuminate\Support\Facades\DB;
*/ */
class JointPermissionBuilder class JointPermissionBuilder
{ {
public function __construct(
protected EntityQueries $queries,
) {
}
/** /**
* Re-generate all entity permission from scratch. * Re-generate all entity permission from scratch.
*/ */
@ -36,7 +42,7 @@ class JointPermissionBuilder
}); });
// Chunk through all bookshelves // 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) { ->chunk(50, function (EloquentCollection $shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles); $this->createManyJointPermissions($shelves->all(), $roles);
}); });
@ -88,7 +94,7 @@ class JointPermissionBuilder
}); });
// Chunk through all bookshelves // Chunk through all bookshelves
Bookshelf::query()->select(['id', 'owned_by']) $this->queries->shelves->start()->select(['id', 'owned_by'])
->chunk(100, function ($shelves) use ($roles) { ->chunk(100, function ($shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles); $this->createManyJointPermissions($shelves->all(), $roles);
}); });
@ -99,7 +105,7 @@ class JointPermissionBuilder
*/ */
protected function bookFetchQuery(): Builder protected function bookFetchQuery(): Builder
{ {
return Book::query()->withTrashed() return $this->queries->books->start()->withTrashed()
->select(['id', 'owned_by'])->with([ ->select(['id', 'owned_by'])->with([
'chapters' => function ($query) { 'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id']); $query->withTrashed()->select(['id', 'owned_by', 'book_id']);

View file

@ -2,10 +2,7 @@
namespace BookStack\Permissions; namespace BookStack\Permissions;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Permissions\Models\EntityPermission; use BookStack\Permissions\Models\EntityPermission;
@ -14,19 +11,18 @@ use Illuminate\Http\Request;
class PermissionsController extends Controller class PermissionsController extends Controller
{ {
protected PermissionsUpdater $permissionsUpdater; public function __construct(
protected PermissionsUpdater $permissionsUpdater,
public function __construct(PermissionsUpdater $permissionsUpdater) protected EntityQueries $queries,
{ ) {
$this->permissionsUpdater = $permissionsUpdater;
} }
/** /**
* Show the Permissions view for a page. * Show the permissions view for a page.
*/ */
public function showForPage(string $bookSlug, string $pageSlug) 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->checkOwnablePermission('restrictions-manage', $page);
$this->setPageTitle(trans('entities.pages_permissions')); $this->setPageTitle(trans('entities.pages_permissions'));
@ -41,7 +37,7 @@ class PermissionsController extends Controller
*/ */
public function updateForPage(Request $request, string $bookSlug, string $pageSlug) 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->checkOwnablePermission('restrictions-manage', $page);
$this->permissionsUpdater->updateFromPermissionsForm($page, $request); $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) 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->checkOwnablePermission('restrictions-manage', $chapter);
$this->setPageTitle(trans('entities.chapters_permissions')); $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) 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->checkOwnablePermission('restrictions-manage', $chapter);
$this->permissionsUpdater->updateFromPermissionsForm($chapter, $request); $this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
@ -86,7 +82,7 @@ class PermissionsController extends Controller
*/ */
public function showForBook(string $slug) public function showForBook(string $slug)
{ {
$book = Book::getBySlug($slug); $book = $this->queries->books->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $book); $this->checkOwnablePermission('restrictions-manage', $book);
$this->setPageTitle(trans('entities.books_permissions')); $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) public function updateForBook(Request $request, string $slug)
{ {
$book = Book::getBySlug($slug); $book = $this->queries->books->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $book); $this->checkOwnablePermission('restrictions-manage', $book);
$this->permissionsUpdater->updateFromPermissionsForm($book, $request); $this->permissionsUpdater->updateFromPermissionsForm($book, $request);
@ -116,7 +112,7 @@ class PermissionsController extends Controller
*/ */
public function showForShelf(string $slug) public function showForShelf(string $slug)
{ {
$shelf = Bookshelf::getBySlug($slug); $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf); $this->checkOwnablePermission('restrictions-manage', $shelf);
$this->setPageTitle(trans('entities.shelves_permissions')); $this->setPageTitle(trans('entities.shelves_permissions'));
@ -131,7 +127,7 @@ class PermissionsController extends Controller
*/ */
public function updateForShelf(Request $request, string $slug) public function updateForShelf(Request $request, string $slug)
{ {
$shelf = Bookshelf::getBySlug($slug); $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf); $this->checkOwnablePermission('restrictions-manage', $shelf);
$this->permissionsUpdater->updateFromPermissionsForm($shelf, $request); $this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
@ -146,7 +142,7 @@ class PermissionsController extends Controller
*/ */
public function copyShelfPermissionsToBooks(string $slug) public function copyShelfPermissionsToBooks(string $slug)
{ {
$shelf = Bookshelf::getBySlug($slug); $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf); $this->checkOwnablePermission('restrictions-manage', $shelf);
$updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf); $updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);

View file

@ -3,6 +3,7 @@
namespace BookStack\References; namespace BookStack\References;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\References\ModelResolvers\BookLinkModelResolver; use BookStack\References\ModelResolvers\BookLinkModelResolver;
use BookStack\References\ModelResolvers\BookshelfLinkModelResolver; use BookStack\References\ModelResolvers\BookshelfLinkModelResolver;
use BookStack\References\ModelResolvers\ChapterLinkModelResolver; use BookStack\References\ModelResolvers\ChapterLinkModelResolver;
@ -85,12 +86,14 @@ class CrossLinkParser
*/ */
public static function createWithEntityResolvers(): self public static function createWithEntityResolvers(): self
{ {
$queries = app()->make(EntityQueries::class);
return new self([ return new self([
new PagePermalinkModelResolver(), new PagePermalinkModelResolver($queries->pages),
new PageLinkModelResolver(), new PageLinkModelResolver($queries->pages),
new ChapterLinkModelResolver(), new ChapterLinkModelResolver($queries->chapters),
new BookLinkModelResolver(), new BookLinkModelResolver($queries->books),
new BookshelfLinkModelResolver(), new BookshelfLinkModelResolver($queries->shelves),
]); ]);
} }
} }

View file

@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Queries\BookQueries;
class BookLinkModelResolver implements CrossLinkModelResolver class BookLinkModelResolver implements CrossLinkModelResolver
{ {
public function __construct(
protected BookQueries $queries
) {
}
public function resolve(string $link): ?Model public function resolve(string $link): ?Model
{ {
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '([#?\/]|$)/'; $pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
@ -19,7 +25,7 @@ class BookLinkModelResolver implements CrossLinkModelResolver
$bookSlug = $matches[1]; $bookSlug = $matches[1];
/** @var ?Book $model */ /** @var ?Book $model */
$model = Book::query()->where('slug', '=', $bookSlug)->first(['id']); $model = $this->queries->start()->where('slug', '=', $bookSlug)->first(['id']);
return $model; return $model;
} }

View file

@ -4,9 +4,14 @@ namespace BookStack\References\ModelResolvers;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookshelfQueries;
class BookshelfLinkModelResolver implements CrossLinkModelResolver class BookshelfLinkModelResolver implements CrossLinkModelResolver
{ {
public function __construct(
protected BookshelfQueries $queries
) {
}
public function resolve(string $link): ?Model public function resolve(string $link): ?Model
{ {
$pattern = '/^' . preg_quote(url('/shelves'), '/') . '\/([\w-]+)' . '([#?\/]|$)/'; $pattern = '/^' . preg_quote(url('/shelves'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
@ -19,7 +24,7 @@ class BookshelfLinkModelResolver implements CrossLinkModelResolver
$shelfSlug = $matches[1]; $shelfSlug = $matches[1];
/** @var ?Bookshelf $model */ /** @var ?Bookshelf $model */
$model = Bookshelf::query()->where('slug', '=', $shelfSlug)->first(['id']); $model = $this->queries->start()->where('slug', '=', $shelfSlug)->first(['id']);
return $model; return $model;
} }

View file

@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\ChapterQueries;
class ChapterLinkModelResolver implements CrossLinkModelResolver class ChapterLinkModelResolver implements CrossLinkModelResolver
{ {
public function __construct(
protected ChapterQueries $queries
) {
}
public function resolve(string $link): ?Model public function resolve(string $link): ?Model
{ {
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/chapter\/' . '([\w-]+)' . '([#?\/]|$)/'; $pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/chapter\/' . '([\w-]+)' . '([#?\/]|$)/';
@ -20,7 +26,7 @@ class ChapterLinkModelResolver implements CrossLinkModelResolver
$chapterSlug = $matches[2]; $chapterSlug = $matches[2];
/** @var ?Chapter $model */ /** @var ?Chapter $model */
$model = Chapter::query()->whereSlugs($bookSlug, $chapterSlug)->first(['id']); $model = $this->queries->usingSlugs($bookSlug, $chapterSlug)->first(['id']);
return $model; return $model;
} }

View file

@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\PageQueries;
class PageLinkModelResolver implements CrossLinkModelResolver class PageLinkModelResolver implements CrossLinkModelResolver
{ {
public function __construct(
protected PageQueries $queries
) {
}
public function resolve(string $link): ?Model public function resolve(string $link): ?Model
{ {
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/page\/' . '([\w-]+)' . '([#?\/]|$)/'; $pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/page\/' . '([\w-]+)' . '([#?\/]|$)/';
@ -20,7 +26,7 @@ class PageLinkModelResolver implements CrossLinkModelResolver
$pageSlug = $matches[2]; $pageSlug = $matches[2];
/** @var ?Page $model */ /** @var ?Page $model */
$model = Page::query()->whereSlugs($bookSlug, $pageSlug)->first(['id']); $model = $this->queries->usingSlugs($bookSlug, $pageSlug)->first(['id']);
return $model; return $model;
} }

View file

@ -4,9 +4,15 @@ namespace BookStack\References\ModelResolvers;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\PageQueries;
class PagePermalinkModelResolver implements CrossLinkModelResolver class PagePermalinkModelResolver implements CrossLinkModelResolver
{ {
public function __construct(
protected PageQueries $queries
) {
}
public function resolve(string $link): ?Model public function resolve(string $link): ?Model
{ {
$pattern = '/^' . preg_quote(url('/link'), '/') . '\/(\d+)/'; $pattern = '/^' . preg_quote(url('/link'), '/') . '\/(\d+)/';
@ -18,7 +24,7 @@ class PagePermalinkModelResolver implements CrossLinkModelResolver
$id = intval($matches[1]); $id = intval($matches[1]);
/** @var ?Page $model */ /** @var ?Page $model */
$model = Page::query()->find($id, ['id']); $model = $this->queries->start()->find($id, ['id']);
return $model; return $model;
} }

View file

@ -2,16 +2,14 @@
namespace BookStack\References; namespace BookStack\References;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Http\Controller; use BookStack\Http\Controller;
class ReferenceController extends Controller class ReferenceController extends Controller
{ {
public function __construct( 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) public function page(string $bookSlug, string $pageSlug)
{ {
$page = Page::getBySlugs($bookSlug, $pageSlug); $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$references = $this->referenceFetcher->getReferencesToEntity($page); $references = $this->referenceFetcher->getReferencesToEntity($page);
return view('pages.references', [ return view('pages.references', [
@ -34,7 +32,7 @@ class ReferenceController extends Controller
*/ */
public function chapter(string $bookSlug, string $chapterSlug) public function chapter(string $bookSlug, string $chapterSlug)
{ {
$chapter = Chapter::getBySlugs($bookSlug, $chapterSlug); $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$references = $this->referenceFetcher->getReferencesToEntity($chapter); $references = $this->referenceFetcher->getReferencesToEntity($chapter);
return view('chapters.references', [ return view('chapters.references', [
@ -48,7 +46,7 @@ class ReferenceController extends Controller
*/ */
public function book(string $slug) public function book(string $slug)
{ {
$book = Book::getBySlug($slug); $book = $this->queries->books->findVisibleBySlugOrFail($slug);
$references = $this->referenceFetcher->getReferencesToEntity($book); $references = $this->referenceFetcher->getReferencesToEntity($book);
return view('books.references', [ return view('books.references', [
@ -62,7 +60,7 @@ class ReferenceController extends Controller
*/ */
public function shelf(string $slug) public function shelf(string $slug)
{ {
$shelf = Bookshelf::getBySlug($slug); $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$references = $this->referenceFetcher->getReferencesToEntity($shelf); $references = $this->referenceFetcher->getReferencesToEntity($shelf);
return view('shelves.references', [ return view('shelves.references', [

View file

@ -23,7 +23,7 @@ class ReferenceFetcher
public function getReferencesToEntity(Entity $entity): Collection public function getReferencesToEntity(Entity $entity): Collection
{ {
$references = $this->queryReferencesToEntity($entity)->get(); $references = $this->queryReferencesToEntity($entity)->get();
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from'); $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', true);
return $references; return $references;
} }

View file

@ -2,8 +2,8 @@
namespace BookStack\Search; namespace BookStack\Search;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Queries\Popular; use BookStack\Entities\Queries\QueryPopular;
use BookStack\Entities\Tools\SiblingFetcher; use BookStack\Entities\Tools\SiblingFetcher;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -11,7 +11,8 @@ use Illuminate\Http\Request;
class SearchController extends Controller class SearchController extends Controller
{ {
public function __construct( 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. * 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. * 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']; $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false); $searchTerm = $request->get('term', false);
@ -77,7 +78,7 @@ class SearchController extends Controller
$searchTerm .= ' {type:' . implode('|', $entityTypes) . '}'; $searchTerm .= ' {type:' . implode('|', $entityTypes) . '}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results']; $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results'];
} else { } 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]); return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);
@ -95,12 +96,11 @@ class SearchController extends Controller
$searchOptions->setFilter('is_template'); $searchOptions->setFilter('is_template');
$entities = $this->searchRunner->searchEntities($searchOptions, 'page', 1, 20)['results']; $entities = $this->searchRunner->searchEntities($searchOptions, 'page', 1, 20)['results'];
} else { } else {
$entities = Page::visible() $entities = $this->pageQueries->visibleTemplates()
->where('template', '=', true)
->where('draft', '=', false) ->where('draft', '=', false)
->orderBy('updated_at', 'desc') ->orderBy('updated_at', 'desc')
->take(20) ->take(20)
->get(Page::$listAttributes); ->get();
} }
return view('search.parts.entity-selector-list', [ return view('search.parts.entity-selector-list', [
@ -130,12 +130,12 @@ class SearchController extends Controller
/** /**
* Search siblings items in the system. * Search siblings items in the system.
*/ */
public function searchSiblings(Request $request) public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
{ {
$type = $request->get('entity_type', null); $type = $request->get('entity_type', null);
$id = $request->get('entity_id', 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']); return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
} }

View file

@ -3,9 +3,9 @@
namespace BookStack\Search; namespace BookStack\Search;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
@ -20,9 +20,6 @@ use SplObjectStorage;
class SearchRunner class SearchRunner
{ {
protected EntityProvider $entityProvider;
protected PermissionApplicator $permissions;
/** /**
* Acceptable operators to be used in a query. * Acceptable operators to be used in a query.
* *
@ -38,10 +35,11 @@ class SearchRunner
*/ */
protected $termAdjustmentCache; protected $termAdjustmentCache;
public function __construct(EntityProvider $entityProvider, PermissionApplicator $permissions) public function __construct(
{ protected EntityProvider $entityProvider,
$this->entityProvider = $entityProvider; protected PermissionApplicator $permissions,
$this->permissions = $permissions; protected EntityQueries $entityQueries,
) {
$this->termAdjustmentCache = new SplObjectStorage(); $this->termAdjustmentCache = new SplObjectStorage();
} }
@ -72,10 +70,9 @@ class SearchRunner
continue; continue;
} }
$entityModelInstance = $this->entityProvider->get($entityType); $searchQuery = $this->buildQuery($searchOpts, $entityType);
$searchQuery = $this->buildQuery($searchOpts, $entityModelInstance);
$entityTotal = $searchQuery->count(); $entityTotal = $searchQuery->count();
$searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityModelInstance, $page, $count); $searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityType, $page, $count);
if ($entityTotal > ($page * $count)) { if ($entityTotal > ($page * $count)) {
$hasMore = true; $hasMore = true;
@ -108,8 +105,7 @@ class SearchRunner
continue; continue;
} }
$entityModelInstance = $this->entityProvider->get($entityType); $search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$search = $this->buildQuery($opts, $entityModelInstance)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search); $results = $results->merge($search);
} }
@ -122,8 +118,7 @@ class SearchRunner
public function searchChapter(int $chapterId, string $searchString): Collection public function searchChapter(int $chapterId, string $searchString): Collection
{ {
$opts = SearchOptions::fromString($searchString); $opts = SearchOptions::fromString($searchString);
$entityModelInstance = $this->entityProvider->get('page'); $pages = $this->buildQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
$pages = $this->buildQuery($opts, $entityModelInstance)->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score'); 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. * 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']; $relations = ['tags'];
if ($entityModelInstance instanceof BookChild) { if ($entityType === 'page' || $entityType === 'chapter') {
$relations['book'] = function (BelongsTo $query) { $relations['book'] = function (BelongsTo $query) {
$query->scopes('visible'); $query->scopes('visible');
}; };
} }
if ($entityModelInstance instanceof Page) { if ($entityType === 'page') {
$relations['chapter'] = function (BelongsTo $query) { $relations['chapter'] = function (BelongsTo $query) {
$query->scopes('visible'); $query->scopes('visible');
}; };
@ -157,18 +152,13 @@ class SearchRunner
/** /**
* Create a search query for an entity. * 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'); $entityModelInstance = $this->entityProvider->get($entityType);
$entityQuery = $this->entityQueries->visibleForList($entityType);
if ($entityModelInstance instanceof Page) {
$entityQuery->select(array_merge($entityModelInstance::$listAttributes, ['owned_by']));
} else {
$entityQuery->select(['*']);
}
// Handle normal search terms // Handle normal search terms
$this->applyTermSearch($entityQuery, $searchOpts, $entityModelInstance); $this->applyTermSearch($entityQuery, $searchOpts, $entityType);
// Handle exact term matching // Handle exact term matching
foreach ($searchOpts->exacts as $inputTerm) { 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. * 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; $terms = $options->searches;
if (count($terms) === 0) { if (count($terms) === 0) {
@ -216,7 +206,7 @@ class SearchRunner
$subQuery->addBinding($scoreSelect['bindings'], 'select'); $subQuery->addBinding($scoreSelect['bindings'], 'select');
$subQuery->where('entity_type', '=', $entity->getMorphClass()); $subQuery->where('entity_type', '=', $entityType);
$subQuery->where(function (Builder $query) use ($terms) { $subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms as $inputTerm) { foreach ($terms as $inputTerm) {
$inputTerm = str_replace('\\', '\\\\', $inputTerm); $inputTerm = str_replace('\\', '\\\\', $inputTerm);

View file

@ -14,7 +14,7 @@ class MaintenanceController extends Controller
/** /**
* Show the page for application maintenance. * Show the page for application maintenance.
*/ */
public function index() public function index(TrashCan $trashCan)
{ {
$this->checkPermission('settings-manage'); $this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint')); $this->setPageTitle(trans('settings.maint'));
@ -23,7 +23,7 @@ class MaintenanceController extends Controller
$version = trim(file_get_contents(base_path('version'))); $version = trim(file_get_contents(base_path('version')));
// Recycle bin details // Recycle bin details
$recycleStats = (new TrashCan())->getTrashedCounts(); $recycleStats = $trashCan->getTrashedCounts();
return view('settings.maintenance', [ return view('settings.maintenance', [
'version' => $version, 'version' => $version,

View file

@ -4,7 +4,6 @@ namespace BookStack\Uploads;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use Exception; use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as Storage; use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemManager; use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;

View file

@ -2,7 +2,7 @@
namespace BookStack\Uploads\Controllers; namespace BookStack\Uploads\Controllers;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use BookStack\Uploads\Attachment; use BookStack\Uploads\Attachment;
@ -15,7 +15,8 @@ use Illuminate\Validation\ValidationException;
class AttachmentApiController extends ApiController class AttachmentApiController extends ApiController
{ {
public function __construct( 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']); $requestData = $this->validate($request, $this->rules()['create']);
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = Page::visible()->findOrFail($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
if ($request->hasFile('file')) { if ($request->hasFile('file')) {
@ -132,7 +133,7 @@ class AttachmentApiController extends ApiController
$page = $attachment->page; $page = $attachment->page;
if ($requestData['uploaded_to'] ?? false) { if ($requestData['uploaded_to'] ?? false) {
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = Page::visible()->findOrFail($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$attachment->uploaded_to = $requestData['uploaded_to']; $attachment->uploaded_to = $requestData['uploaded_to'];
} }

View file

@ -2,6 +2,7 @@
namespace BookStack\Uploads\Controllers; namespace BookStack\Uploads\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
@ -18,6 +19,7 @@ class AttachmentController extends Controller
{ {
public function __construct( public function __construct(
protected AttachmentService $attachmentService, protected AttachmentService $attachmentService,
protected PageQueries $pageQueries,
protected PageRepo $pageRepo protected PageRepo $pageRepo
) { ) {
} }
@ -36,7 +38,7 @@ class AttachmentController extends Controller
]); ]);
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkPermission('attachment-create-all'); $this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
@ -152,7 +154,7 @@ class AttachmentController extends Controller
]), 422); ]), 422);
} }
$page = $this->pageRepo->getById($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkPermission('attachment-create-all'); $this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
@ -173,7 +175,7 @@ class AttachmentController extends Controller
*/ */
public function listForPage(int $pageId) public function listForPage(int $pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
return view('attachments.manager-list', [ return view('attachments.manager-list', [
@ -192,7 +194,7 @@ class AttachmentController extends Controller
$this->validate($request, [ $this->validate($request, [
'order' => ['required', 'array'], 'order' => ['required', 'array'],
]); ]);
$page = $this->pageRepo->getById($pageId); $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$attachmentOrder = $request->get('order'); $attachmentOrder = $request->get('order');
@ -213,7 +215,7 @@ class AttachmentController extends Controller
$attachment = Attachment::query()->findOrFail($attachmentId); $attachment = Attachment::query()->findOrFail($attachmentId);
try { try {
$page = $this->pageRepo->getById($attachment->uploaded_to); $page = $this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);
} catch (NotFoundException $exception) { } catch (NotFoundException $exception) {
throw new NotFoundException(trans('errors.attachment_not_found')); throw new NotFoundException(trans('errors.attachment_not_found'));
} }

View file

@ -8,8 +8,6 @@ use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageResizer; use BookStack\Uploads\ImageResizer;
use BookStack\Util\OutOfMemoryHandler; use BookStack\Util\OutOfMemoryHandler;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller class GalleryImageController extends Controller

View file

@ -2,7 +2,7 @@
namespace BookStack\Uploads\Controllers; namespace BookStack\Uploads\Controllers;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use BookStack\Uploads\Image; use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageRepo;
@ -18,6 +18,7 @@ class ImageGalleryApiController extends ApiController
public function __construct( public function __construct(
protected ImageRepo $imageRepo, protected ImageRepo $imageRepo,
protected ImageResizer $imageResizer, protected ImageResizer $imageResizer,
protected PageQueries $pageQueries,
) { ) {
} }
@ -66,9 +67,9 @@ class ImageGalleryApiController extends ApiController
{ {
$this->checkPermission('image-create-all'); $this->checkPermission('image-create-all');
$data = $this->validate($request, $this->rules()['create']); $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'])) { if (isset($data['name'])) {
$image->refresh(); $image->refresh();

View file

@ -2,7 +2,7 @@
namespace BookStack\Uploads; namespace BookStack\Uploads;
use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Exception; use Exception;
@ -15,6 +15,7 @@ class ImageRepo
protected ImageService $imageService, protected ImageService $imageService,
protected PermissionApplicator $permissions, protected PermissionApplicator $permissions,
protected ImageResizer $imageResizer, protected ImageResizer $imageResizer,
protected PageQueries $pageQueries,
) { ) {
} }
@ -77,14 +78,13 @@ class ImageRepo
*/ */
public function getEntityFiltered( public function getEntityFiltered(
string $type, string $type,
string $filterType = null, ?string $filterType,
int $page = 0, int $page,
int $pageSize = 24, int $pageSize,
int $uploadedTo = null, int $uploadedTo,
string $search = null ?string $search
): array { ): array {
/** @var Page $contextPage */ $contextPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo);
$contextPage = Page::visible()->findOrFail($uploadedTo);
$parentFilter = null; $parentFilter = null;
if ($filterType === 'book' || $filterType === 'page') { if ($filterType === 'book' || $filterType === 'page') {
@ -225,9 +225,9 @@ class ImageRepo
*/ */
public function getPagesUsingImage(Image $image): array public function getPagesUsingImage(Image $image): array
{ {
$pages = Page::visible() $pages = $this->pageQueries->visibleForList()
->where('html', 'like', '%' . $image->url . '%') ->where('html', 'like', '%' . $image->url . '%')
->get(['id', 'name', 'slug', 'book_id']); ->get();
foreach ($pages as $page) { foreach ($pages as $page) {
$page->setAttribute('url', $page->getUrl()); $page->setAttribute('url', $page->getUrl());

View file

@ -2,9 +2,7 @@
namespace BookStack\Uploads; namespace BookStack\Uploads;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use Exception; use Exception;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -20,6 +18,7 @@ class ImageService
public function __construct( public function __construct(
protected ImageStorage $storage, protected ImageStorage $storage,
protected ImageResizer $resizer, protected ImageResizer $resizer,
protected EntityQueries $queries,
) { ) {
} }
@ -278,15 +277,15 @@ class ImageService
} }
if ($imageType === 'gallery' || $imageType === 'drawio') { 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') { 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') { 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; return false;

View file

@ -10,16 +10,25 @@ use BookStack\Users\UserRepo;
class UserProfileController extends Controller class UserProfileController extends Controller
{ {
public function __construct(
protected UserRepo $userRepo,
protected ActivityQueries $activityQueries,
protected UserContentCounts $contentCounts,
protected UserRecentlyCreatedContent $recentlyCreatedContent
) {
}
/** /**
* Show the user profile page. * 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); $userActivity = $this->activityQueries->userActivity($user);
$recentlyCreated = (new UserRecentlyCreatedContent())->run($user, 5); $recentlyCreated = $this->recentlyCreatedContent->run($user, 5);
$assetCounts = (new UserContentCounts())->run($user); $assetCounts = $this->contentCounts->run($user);
$this->setPageTitle($user->name); $this->setPageTitle($user->name);

View file

@ -2,10 +2,7 @@
namespace BookStack\Users\Queries; namespace BookStack\Users\Queries;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
/** /**
@ -13,6 +10,12 @@ use BookStack\Users\Models\User;
*/ */
class UserContentCounts class UserContentCounts
{ {
public function __construct(
protected EntityQueries $queries,
) {
}
/** /**
* @return array{pages: int, chapters: int, books: int, shelves: int} * @return array{pages: int, chapters: int, books: int, shelves: int}
*/ */
@ -21,10 +24,10 @@ class UserContentCounts
$createdBy = ['created_by' => $user->id]; $createdBy = ['created_by' => $user->id];
return [ return [
'pages' => Page::visible()->where($createdBy)->count(), 'pages' => $this->queries->pages->visibleForList()->where($createdBy)->count(),
'chapters' => Chapter::visible()->where($createdBy)->count(), 'chapters' => $this->queries->chapters->visibleForList()->where($createdBy)->count(),
'books' => Book::visible()->where($createdBy)->count(), 'books' => $this->queries->books->visibleForList()->where($createdBy)->count(),
'shelves' => Bookshelf::visible()->where($createdBy)->count(), 'shelves' => $this->queries->shelves->visibleForList()->where($createdBy)->count(),
]; ];
} }
} }

View file

@ -2,10 +2,7 @@
namespace BookStack\Users\Queries; namespace BookStack\Users\Queries;
use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
@ -15,6 +12,11 @@ use Illuminate\Database\Eloquent\Collection;
*/ */
class UserRecentlyCreatedContent class UserRecentlyCreatedContent
{ {
public function __construct(
protected EntityQueries $queries,
) {
}
/** /**
* @return array{pages: Collection, chapters: Collection, books: Collection, shelves: Collection} * @return array{pages: Collection, chapters: Collection, books: Collection, shelves: Collection}
*/ */
@ -28,10 +30,10 @@ class UserRecentlyCreatedContent
}; };
return [ return [
'pages' => $query(Page::visible()->where('draft', '=', false)), 'pages' => $query($this->queries->pages->visibleForList()->where('draft', '=', false)),
'chapters' => $query(Chapter::visible()), 'chapters' => $query($this->queries->chapters->visibleForList()),
'books' => $query(Book::visible()), 'books' => $query($this->queries->books->visibleForList()),
'shelves' => $query(Bookshelf::visible()), 'shelves' => $query($this->queries->shelves->visibleForList()),
]; ];
} }
} }

View file

@ -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');
});
}
};

View file

@ -1,5 +1,5 @@
@extends('layouts.simple') @extends('layouts.simple')
@inject('popular', \BookStack\Entities\Queries\QueryPopular::class)
@section('content') @section('content')
<div class="container mt-l"> <div class="container mt-l">
@ -28,7 +28,7 @@
<div class="card mb-xl"> <div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.pages_popular') }}</h3> <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
<div class="px-m"> <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> </div>
</div> </div>
@ -36,7 +36,7 @@
<div class="card mb-xl"> <div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.books_popular') }}</h3> <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
<div class="px-m"> <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> </div>
</div> </div>
@ -44,7 +44,7 @@
<div class="card mb-xl"> <div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3> <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
<div class="px-m"> <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> </div>
</div> </div>

View file

@ -24,6 +24,9 @@ class BooksApiTest extends TestCase
'id' => $firstBook->id, 'id' => $firstBook->id,
'name' => $firstBook->name, 'name' => $firstBook->name,
'slug' => $firstBook->slug, 'slug' => $firstBook->slug,
'owned_by' => $firstBook->owned_by,
'created_by' => $firstBook->created_by,
'updated_by' => $firstBook->updated_by,
], ],
]]); ]]);
} }

View file

@ -28,6 +28,9 @@ class ChaptersApiTest extends TestCase
'book_id' => $firstChapter->book->id, 'book_id' => $firstChapter->book->id,
'priority' => $firstChapter->priority, 'priority' => $firstChapter->priority,
'book_slug' => $firstChapter->book->slug, '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, 'id' => $page->id,
'slug' => $page->slug, 'slug' => $page->slug,
'name' => $page->name, '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, 'default_template_id' => null,

View file

@ -27,6 +27,10 @@ class PagesApiTest extends TestCase
'slug' => $firstPage->slug, 'slug' => $firstPage->slug,
'book_id' => $firstPage->book->id, 'book_id' => $firstPage->book->id,
'priority' => $firstPage->priority, 'priority' => $firstPage->priority,
'owned_by' => $firstPage->owned_by,
'created_by' => $firstPage->created_by,
'updated_by' => $firstPage->updated_by,
'revision_count' => $firstPage->revision_count,
], ],
]]); ]]);
} }

View file

@ -25,6 +25,9 @@ class ShelvesApiTest extends TestCase
'id' => $firstBookshelf->id, 'id' => $firstBookshelf->id,
'name' => $firstBookshelf->name, 'name' => $firstBookshelf->name,
'slug' => $firstBookshelf->slug, 'slug' => $firstBookshelf->slug,
'owned_by' => $firstBookshelf->owned_by,
'created_by' => $firstBookshelf->created_by,
'updated_by' => $firstBookshelf->updated_by,
], ],
]]); ]]);
} }

View file

@ -317,7 +317,7 @@ class BookTest extends TestCase
$copy = Book::query()->where('name', '=', 'My copy book')->first(); $copy = Book::query()->where('name', '=', 'My copy book')->first();
$resp->assertRedirect($copy->getUrl()); $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); $this->get($copy->getUrl())->assertSee($book->description_html, false);
} }
@ -329,7 +329,7 @@ class BookTest extends TestCase
// Hide child content // Hide child content
/** @var BookChild $page */ /** @var BookChild $page */
foreach ($book->getDirectChildren() as $child) { foreach ($book->getDirectVisibleChildren() as $child) {
$this->permissions->setEntityPermissions($child, [], []); $this->permissions->setEntityPermissions($child, [], []);
} }
@ -337,7 +337,7 @@ class BookTest extends TestCase
/** @var Book $copy */ /** @var Book $copy */
$copy = Book::query()->where('name', '=', 'My copy book')->first(); $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() public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()

View file

@ -303,7 +303,7 @@ class EntitySearchTest extends TestCase
public function test_sibling_search_for_pages_without_chapter() public function test_sibling_search_for_pages_without_chapter()
{ {
$page = $this->entities->pageNotWithinChapter(); $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'); $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"); $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() public function test_sibling_search_for_chapters()
{ {
$chapter = $this->entities->chapter(); $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'); $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"); $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter");