use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
class ActivityQueries
{
- protected PermissionApplicator $permissions;
-
- public function __construct(PermissionApplicator $permissions)
- {
- $this->permissions = $permissions;
+ public function __construct(
+ protected PermissionApplicator $permissions,
+ protected MixedEntityListLoader $listLoader,
+ ) {
}
/**
$activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
- ->with(['user', 'entity'])
+ ->with(['user'])
->skip($count * $page)
->take($count)
->get();
+ $this->listLoader->loadIntoRelations($activityList->all(), 'entity', false);
+
return $this->filterSimilar($activityList);
}
namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class CommentController extends Controller
{
public function __construct(
- protected CommentRepo $commentRepo
+ protected CommentRepo $commentRepo,
+ protected PageQueries $pageQueries,
) {
}
'parent_id' => ['nullable', 'integer'],
]);
- $page = Page::visible()->find($pageId);
+ $page = $this->pageQueries->findVisibleById($pageId);
if ($page === null) {
return response('Not found', 404);
}
namespace BookStack\Activity\Controllers;
-use BookStack\Entities\Queries\TopFavourites;
+use BookStack\Entities\Queries\QueryTopFavourites;
use BookStack\Entities\Tools\MixedEntityRequestHelper;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
/**
* Show a listing of all favourite items for the current user.
*/
- public function index(Request $request)
+ public function index(Request $request, QueryTopFavourites $topFavourites)
{
$viewCount = 20;
$page = intval($request->get('page', 1));
- $favourites = (new TopFavourites())->run($viewCount + 1, (($page - 1) * $viewCount));
+ $favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount));
$hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
namespace BookStack\App;
use BookStack\Activity\ActivityQueries;
-use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
-use BookStack\Entities\Queries\RecentlyViewed;
-use BookStack\Entities\Queries\TopFavourites;
-use BookStack\Entities\Repos\BookRepo;
-use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Entities\Queries\QueryRecentlyViewed;
+use BookStack\Entities\Queries\QueryTopFavourites;
use BookStack\Entities\Tools\PageContent;
use BookStack\Http\Controller;
use BookStack\Uploads\FaviconHandler;
class HomeController extends Controller
{
+ public function __construct(
+ protected EntityQueries $queries,
+ ) {
+ }
+
/**
* Display the homepage.
*/
- public function index(Request $request, ActivityQueries $activities)
- {
+ public function index(
+ Request $request,
+ ActivityQueries $activities,
+ QueryRecentlyViewed $recentlyViewed,
+ QueryTopFavourites $topFavourites,
+ ) {
$activity = $activities->latest(10);
$draftPages = [];
if ($this->isSignedIn()) {
- $draftPages = Page::visible()
- ->where('draft', '=', true)
- ->where('created_by', '=', user()->id)
+ $draftPages = $this->queries->pages->currentUserDraftsForList()
->orderBy('updated_at', 'desc')
->with('book')
->take(6)
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ?
- (new RecentlyViewed())->run(12 * $recentFactor, 1)
- : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
- $favourites = (new TopFavourites())->run(6);
- $recentlyUpdatedPages = Page::visible()->with('book')
+ $recentlyViewed->run(12 * $recentFactor, 1)
+ : $this->queries->books->visibleForList()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
+ $favourites = $topFavourites->run(6);
+ $recentlyUpdatedPages = $this->queries->pages->visibleForList()
->where('draft', false)
->orderBy('updated_at', 'desc')
->take($favourites->count() > 0 ? 5 : 10)
- ->select(Page::$listAttributes)
->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
}
if ($homepageOption === 'bookshelves') {
- $shelves = app()->make(BookshelfRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
+ $shelves = $this->queries->shelves->visibleForListWithCover()
+ ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
+ ->paginate(18);
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('home.shelves', $data);
}
if ($homepageOption === 'books') {
- $books = app()->make(BookRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
+ $books = $this->queries->books->visibleForListWithCover()
+ ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
+ ->paginate(18);
$data = array_merge($commonData, ['books' => $books]);
return view('home.books', $data);
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
/** @var Page $customHomepage */
- $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
+ $customHomepage = $this->queries->pages->start()->where('draft', '=', false)->findOrFail($id);
$pageContent = new PageContent($customHomepage);
$customHomepage->html = $pageContent->render(false);
use BookStack\Theming\ThemeEvents;
use BookStack\Theming\ThemeService;
-use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class ThemeServiceProvider extends ServiceProvider
// List of URIs that should not be collected
'except' => [
+ '/uploads/images/.*', // BookStack image requests
+
'/horizon/.*', // Laravel Horizon requests
'/telescope/.*', // Laravel Telescope requests
'/_debugbar/.*', // Laravel DebugBar requests
namespace BookStack\Console\Commands;
-use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Tools\PermissionsUpdater;
use Illuminate\Console\Command;
/**
* Execute the console command.
*/
- public function handle(PermissionsUpdater $permissionsUpdater): int
+ public function handle(PermissionsUpdater $permissionsUpdater, BookshelfQueries $queries): int
{
$shelfSlug = $this->option('slug');
$cascadeAll = $this->option('all');
return 0;
}
- $shelves = Bookshelf::query()->get(['id']);
+ $shelves = $queries->start()->get(['id']);
}
if ($shelfSlug) {
- $shelves = Bookshelf::query()->where('slug', '=', $shelfSlug)->get(['id']);
+ $shelves = $queries->start()->where('slug', '=', $shelfSlug)->get(['id']);
if ($shelves->count() === 0) {
$this->info('No shelves found with the given slug.');
}
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Http\ApiController;
class BookApiController extends ApiController
{
public function __construct(
- protected BookRepo $bookRepo
+ protected BookRepo $bookRepo,
+ protected BookQueries $queries,
) {
}
*/
public function list()
{
- $books = Book::visible();
+ $books = $this->queries
+ ->visibleForList()
+ ->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($books, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
*/
public function read(string $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail(intval($id));
$book = $this->forJsonDisplay($book);
$book->load(['createdBy', 'updatedBy', 'ownedBy']);
*/
public function update(Request $request, string $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('book-update', $book);
$requestData = $this->validate($request, $this->rules()['update']);
*/
public function delete(string $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions;
-use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookQueries;
+use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
public function __construct(
protected ShelfContext $shelfContext,
protected BookRepo $bookRepo,
- protected ReferenceFetcher $referenceFetcher
+ protected BookQueries $queries,
+ protected BookshelfQueries $shelfQueries,
+ protected ReferenceFetcher $referenceFetcher,
) {
}
'updated_at' => trans('common.sort_updated_at'),
]);
- $books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
- $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
- $popular = $this->bookRepo->getPopular(4);
- $new = $this->bookRepo->getRecentlyCreated(4);
+ $books = $this->queries->visibleForListWithCover()
+ ->orderBy($listOptions->getSort(), $listOptions->getOrder())
+ ->paginate(18);
+ $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false;
+ $popular = $this->queries->popularForList()->take(4)->get();
+ $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get();
$this->shelfContext->clearShelfContext();
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
+ $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
+ $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
- $book = $this->bookRepo->getBySlug($slug);
+ $book = $this->queries->findVisibleBySlugOrFail($slug);
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->scopes('visible')->get();
*/
public function edit(string $slug)
{
- $book = $this->bookRepo->getBySlug($slug);
+ $book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
*/
public function update(Request $request, string $slug)
{
- $book = $this->bookRepo->getBySlug($slug);
+ $book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('book-update', $book);
$validated = $this->validate($request, [
*/
public function showDelete(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
*/
public function destroy(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
*/
public function showCopy(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-view', $book);
session()->flashInput(['name' => $book->name]);
*/
public function copy(Request $request, Cloner $cloner, string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-view', $book);
$this->checkPermission('book-create-all');
*/
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission('book-delete', $book);
$this->checkPermission('bookshelf-create-all');
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;
class BookExportApiController extends ApiController
{
- protected $exportFormatter;
-
- public function __construct(ExportFormatter $exportFormatter)
- {
- $this->exportFormatter = $exportFormatter;
+ public function __construct(
+ protected ExportFormatter $exportFormatter,
+ protected BookQueries $queries,
+ ) {
$this->middleware('can:content-export');
}
*/
public function exportPdf(int $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail($id);
$pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->download()->directly($pdfContent, $book->slug . '.pdf');
*/
public function exportHtml(int $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail($id);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->download()->directly($htmlContent, $book->slug . '.html');
*/
public function exportPlainText(int $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail($id);
$textContent = $this->exportFormatter->bookToPlainText($book);
return $this->download()->directly($textContent, $book->slug . '.txt');
*/
public function exportMarkdown(int $id)
{
- $book = Book::visible()->findOrFail($id);
+ $book = $this->queries->findVisibleByIdOrFail($id);
$markdown = $this->exportFormatter->bookToMarkdown($book);
return $this->download()->directly($markdown, $book->slug . '.md');
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\Controller;
use Throwable;
class BookExportController extends Controller
{
- protected $bookRepo;
- protected $exportFormatter;
-
- /**
- * BookExportController constructor.
- */
- public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
- {
- $this->bookRepo = $bookRepo;
- $this->exportFormatter = $exportFormatter;
+ public function __construct(
+ protected BookQueries $queries,
+ protected ExportFormatter $exportFormatter,
+ ) {
$this->middleware('can:content-export');
}
*/
public function pdf(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
*/
public function html(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->download()->directly($htmlContent, $bookSlug . '.html');
*/
public function plainText(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$textContent = $this->exportFormatter->bookToPlainText($book);
return $this->download()->directly($textContent, $bookSlug . '.txt');
*/
public function markdown(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$textContent = $this->exportFormatter->bookToMarkdown($book);
return $this->download()->directly($textContent, $bookSlug . '.md');
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType;
-use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity;
class BookSortController extends Controller
{
- protected $bookRepo;
-
- public function __construct(BookRepo $bookRepo)
- {
- $this->bookRepo = $bookRepo;
+ public function __construct(
+ protected BookQueries $queries,
+ ) {
}
/**
*/
public function show(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = (new BookContents($book))->getTree(false);
*/
public function showItem(string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$bookChildren = (new BookContents($book))->getTree();
return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
*/
public function update(Request $request, string $bookSlug)
{
- $book = $this->bookRepo->getBySlug($bookSlug);
+ $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Http\ApiController;
use Exception;
class BookshelfApiController extends ApiController
{
public function __construct(
- protected BookshelfRepo $bookshelfRepo
+ protected BookshelfRepo $bookshelfRepo,
+ protected BookshelfQueries $queries,
) {
}
*/
public function list()
{
- $shelves = Bookshelf::visible();
+ $shelves = $this->queries
+ ->visibleForList()
+ ->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($shelves, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
*/
public function read(string $id)
{
- $shelf = Bookshelf::visible()->findOrFail($id);
+ $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$shelf = $this->forJsonDisplay($shelf);
$shelf->load([
'createdBy', 'updatedBy', 'ownedBy',
*/
public function update(Request $request, string $id)
{
- $shelf = Bookshelf::visible()->findOrFail($id);
+ $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules()['update']);
*/
public function delete(string $id)
{
- $shelf = Bookshelf::visible()->findOrFail($id);
+ $shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf);
use BookStack\Activity\ActivityQueries;
use BookStack\Activity\Models\View;
-use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\BookQueries;
+use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
{
public function __construct(
protected BookshelfRepo $shelfRepo,
+ protected BookshelfQueries $queries,
+ protected BookQueries $bookQueries,
protected ShelfContext $shelfContext,
- protected ReferenceFetcher $referenceFetcher
+ protected ReferenceFetcher $referenceFetcher,
) {
}
'updated_at' => trans('common.sort_updated_at'),
]);
- $shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
- $recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
- $popular = $this->shelfRepo->getPopular(4);
- $new = $this->shelfRepo->getRecentlyCreated(4);
+ $shelves = $this->queries->visibleForListWithCover()
+ ->orderBy($listOptions->getSort(), $listOptions->getOrder())
+ ->paginate(18);
+ $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false;
+ $popular = $this->queries->popularForList()->get();
+ $new = $this->queries->visibleForList()
+ ->orderBy('created_at', 'desc')
+ ->take(4)
+ ->get();
$this->shelfContext->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
public function create()
{
$this->checkPermission('bookshelf-create-all');
- $books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
+ $books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]);
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
- $shelf = $this->shelfRepo->getBySlug($slug);
+ $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-view', $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
*/
public function edit(string $slug)
{
- $shelf = $this->shelfRepo->getBySlug($slug);
+ $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
- $books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
+ $books = $this->bookQueries->visibleForList()
+ ->whereNotIn('id', $shelfBookIds)
+ ->orderBy('name')
+ ->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
*/
public function update(Request $request, string $slug)
{
- $shelf = $this->shelfRepo->getBySlug($slug);
+ $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
*/
public function showDelete(string $slug)
{
- $shelf = $this->shelfRepo->getBySlug($slug);
+ $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
*/
public function destroy(string $slug)
{
- $shelf = $this->shelfRepo->getBySlug($slug);
+ $shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->shelfRepo->destroy($shelf);
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\ChapterQueries;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
];
public function __construct(
- protected ChapterRepo $chapterRepo
+ protected ChapterRepo $chapterRepo,
+ protected ChapterQueries $queries,
+ protected EntityQueries $entityQueries,
) {
}
*/
public function list()
{
- $chapters = Chapter::visible();
+ $chapters = $this->queries->visibleForList()
+ ->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority',
$requestData = $this->validate($request, $this->rules['create']);
$bookId = $request->get('book_id');
- $book = Book::visible()->findOrFail($bookId);
+ $book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($requestData, $book);
*/
public function read(string $id)
{
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$chapter = $this->forJsonDisplay($chapter);
- $chapter->load([
- 'createdBy', 'updatedBy', 'ownedBy',
- 'pages' => function (HasMany $query) {
- $query->scopes('visible')->get(['id', 'name', 'slug']);
- }
- ]);
+ $chapter->load(['createdBy', 'updatedBy', 'ownedBy']);
+
+ // Note: More fields than usual here, for backwards compatibility,
+ // due to previously accidentally including more fields that desired.
+ $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)
+ ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor'])
+ ->get();
+ $chapter->setRelation('pages', $pages);
return response()->json($chapter);
}
public function update(Request $request, string $id)
{
$requestData = $this->validate($request, $this->rules()['update']);
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('chapter-update', $chapter);
if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
*/
public function delete(string $id)
{
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\ChapterQueries;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
{
public function __construct(
protected ChapterRepo $chapterRepo,
- protected ReferenceFetcher $referenceFetcher
+ protected ChapterQueries $queries,
+ protected EntityQueries $entityQueries,
+ protected ReferenceFetcher $referenceFetcher,
) {
}
*/
public function create(string $bookSlug)
{
- $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+ $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
- return view('chapters.create', ['book' => $book, 'current' => $book]);
+ return view('chapters.create', [
+ 'book' => $book,
+ 'current' => $book,
+ ]);
}
/**
'default_template_id' => ['nullable', 'integer'],
]);
- $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+ $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($validated, $book);
*/
public function show(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = (new BookContents($chapter->book))->getTree();
- $pages = $chapter->getVisiblePages();
+ $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
+
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
View::incrementFor($chapter);
*/
public function edit(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
'default_template_id' => ['nullable', 'integer'],
]);
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->chapterRepo->update($chapter, $validated);
*/
public function showDelete(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
*/
public function destroy(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
*/
public function showMove(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
*/
public function move(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
*/
public function showCopy(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
session()->flashInput(['name' => $chapter->name]);
*/
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$entitySelection = $request->get('entity_selection') ?: null;
- $newParentBook = $entitySelection ? $this->chapterRepo->findParentByIdentifier($entitySelection) : $chapter->getParent();
+ $newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
- if (is_null($newParentBook)) {
+ if (!$newParentBook instanceof Book) {
$this->showErrorNotification(trans('errors.selected_book_not_found'));
return redirect($chapter->getUrl('/copy'));
*/
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all');
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;
class ChapterExportApiController extends ApiController
{
- protected $exportFormatter;
-
- /**
- * ChapterExportController constructor.
- */
- public function __construct(ExportFormatter $exportFormatter)
- {
- $this->exportFormatter = $exportFormatter;
+ public function __construct(
+ protected ExportFormatter $exportFormatter,
+ protected ChapterQueries $queries,
+ ) {
$this->middleware('can:content-export');
}
*/
public function exportPdf(int $id)
{
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail($id);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
*/
public function exportHtml(int $id)
{
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail($id);
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->download()->directly($htmlContent, $chapter->slug . '.html');
*/
public function exportPlainText(int $id)
{
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail($id);
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
return $this->download()->directly($textContent, $chapter->slug . '.txt');
*/
public function exportMarkdown(int $id)
{
- $chapter = Chapter::visible()->findOrFail($id);
+ $chapter = $this->queries->findVisibleByIdOrFail($id);
$markdown = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->download()->directly($markdown, $chapter->slug . '.md');
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
class ChapterExportController extends Controller
{
- protected $chapterRepo;
- protected $exportFormatter;
-
- /**
- * ChapterExportController constructor.
- */
- public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
- {
- $this->chapterRepo = $chapterRepo;
- $this->exportFormatter = $exportFormatter;
+ public function __construct(
+ protected ChapterQueries $queries,
+ protected ExportFormatter $exportFormatter,
+ ) {
$this->middleware('can:content-export');
}
*/
public function pdf(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
*/
public function html(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->download()->directly($containedHtml, $chapterSlug . '.html');
*/
public function plainText(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToPlainText($chapter);
return $this->download()->directly($chapterText, $chapterSlug . '.txt');
*/
public function markdown(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->download()->directly($chapterText, $chapterSlug . '.md');
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
];
public function __construct(
- protected PageRepo $pageRepo
+ protected PageRepo $pageRepo,
+ protected PageQueries $queries,
+ protected EntityQueries $entityQueries,
) {
}
*/
public function list()
{
- $pages = Page::visible();
+ $pages = $this->queries->visibleForList()
+ ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
return $this->apiListingResponse($pages, [
'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
$this->validate($request, $this->rules['create']);
if ($request->has('chapter_id')) {
- $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+ $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
} else {
- $parent = Book::visible()->findOrFail($request->get('book_id'));
+ $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
}
$this->checkOwnablePermission('page-create', $parent);
*/
public function read(string $id)
{
- $page = $this->pageRepo->getById($id, []);
+ $page = $this->queries->findVisibleByIdOrFail($id);
return response()->json($page->forJsonDisplay());
}
{
$requestData = $this->validate($request, $this->rules['update']);
- $page = $this->pageRepo->getById($id, []);
+ $page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission('page-update', $page);
$parent = null;
if ($request->has('chapter_id')) {
- $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+ $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id')));
} elseif ($request->has('book_id')) {
- $parent = Book::visible()->findOrFail($request->get('book_id'));
+ $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
}
if ($parent && !$parent->matches($page->getParent())) {
*/
public function delete(string $id)
{
- $page = $this->pageRepo->getById($id, []);
+ $page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page);
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
{
public function __construct(
protected PageRepo $pageRepo,
+ protected PageQueries $queries,
+ protected EntityQueries $entityQueries,
protected ReferenceFetcher $referenceFetcher
) {
}
*/
public function create(string $bookSlug, string $chapterSlug = null)
{
- $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
+ if ($chapterSlug) {
+ $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
+ } else {
+ $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
+ }
+
$this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in
'name' => ['required', 'string', 'max:255'],
]);
- $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
+ if ($chapterSlug) {
+ $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
+ } else {
+ $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
+ }
+
$this->checkOwnablePermission('page-create', $parent);
$page = $this->pageRepo->getNewDraftPage($parent);
*/
public function editDraft(Request $request, string $bookSlug, int $pageId)
{
- $draft = $this->pageRepo->getById($pageId);
+ $draft = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-create', $draft->getParent());
- $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
+ $editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
return view('pages.edit', $editorData->getViewData());
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
- $draftPage = $this->pageRepo->getById($pageId);
+ $draftPage = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-create', $draftPage->getParent());
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
public function show(string $bookSlug, string $pageSlug)
{
try {
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
} catch (NotFoundException $e) {
- $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
+ $revision = $this->entityQueries->revisions->findLatestVersionBySlugs($bookSlug, $pageSlug);
+ $page = $revision->page ?? null;
- if ($page === null) {
+ if (is_null($page)) {
throw $e;
}
*/
public function getPageAjax(int $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->queries->findVisibleByIdOrFail($pageId);
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
$page->makeHidden(['book']);
*/
public function edit(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
- $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
+ $editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
if ($editorData->getWarnings()) {
$this->showWarningNotification(implode("\n", $editorData->getWarnings()));
}
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->update($page, $request->all());
*/
public function saveDraft(Request $request, int $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
if (!$this->isSignedIn()) {
*/
public function redirectFromLink(int $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->queries->findVisibleByIdOrFail($pageId);
return redirect($page->getUrl());
}
*/
public function showDelete(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate =
- Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
- Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
+ $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
+ $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
return view('pages.delete', [
'book' => $page->book,
*/
public function showDeleteDraft(string $bookSlug, int $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate =
- Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
- Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
+ $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
+ $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
return view('pages.delete', [
'book' => $page->book,
*/
public function destroy(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$parent = $page->getParent();
*/
public function destroyDraft(string $bookSlug, int $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->queries->findVisibleByIdOrFail($pageId);
$book = $page->book;
$chapter = $page->chapter;
$this->checkOwnablePermission('page-update', $page);
$query->scopes('visible');
};
- $pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
+ $pages = $this->queries->visibleForList()
+ ->addSelect('updated_by')
+ ->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
->orderBy('updated_at', 'desc')
->paginate(20)
->setPath(url('/pages/recently-updated'));
*/
public function showMove(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
*/
public function move(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
*/
public function showCopy(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
*/
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection') ?: null;
- $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
+ $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
- if (is_null($newParent)) {
+ if (!$newParent instanceof Book && !$newParent instanceof Chapter) {
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect($page->getUrl('/copy'));
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;
class PageExportApiController extends ApiController
{
- protected $exportFormatter;
-
- public function __construct(ExportFormatter $exportFormatter)
- {
- $this->exportFormatter = $exportFormatter;
+ public function __construct(
+ protected ExportFormatter $exportFormatter,
+ protected PageQueries $queries,
+ ) {
$this->middleware('can:content-export');
}
*/
public function exportPdf(int $id)
{
- $page = Page::visible()->findOrFail($id);
+ $page = $this->queries->findVisibleByIdOrFail($id);
$pdfContent = $this->exportFormatter->pageToPdf($page);
return $this->download()->directly($pdfContent, $page->slug . '.pdf');
*/
public function exportHtml(int $id)
{
- $page = Page::visible()->findOrFail($id);
+ $page = $this->queries->findVisibleByIdOrFail($id);
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
return $this->download()->directly($htmlContent, $page->slug . '.html');
*/
public function exportPlainText(int $id)
{
- $page = Page::visible()->findOrFail($id);
+ $page = $this->queries->findVisibleByIdOrFail($id);
$textContent = $this->exportFormatter->pageToPlainText($page);
return $this->download()->directly($textContent, $page->slug . '.txt');
*/
public function exportMarkdown(int $id)
{
- $page = Page::visible()->findOrFail($id);
+ $page = $this->queries->findVisibleByIdOrFail($id);
$markdown = $this->exportFormatter->pageToMarkdown($page);
return $this->download()->directly($markdown, $page->slug . '.md');
namespace BookStack\Entities\Controllers;
-use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
class PageExportController extends Controller
{
- protected $pageRepo;
- protected $exportFormatter;
-
- /**
- * PageExportController constructor.
- */
- public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter)
- {
- $this->pageRepo = $pageRepo;
- $this->exportFormatter = $exportFormatter;
+ public function __construct(
+ protected PageQueries $queries,
+ protected ExportFormatter $exportFormatter,
+ ) {
$this->middleware('can:content-export');
}
*/
public function pdf(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
$pdfContent = $this->exportFormatter->pageToPdf($page);
*/
public function html(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
$containedHtml = $this->exportFormatter->pageToContainedHtml($page);
*/
public function plainText(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToPlainText($page);
return $this->download()->directly($pageText, $pageSlug . '.txt');
*/
public function markdown(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToMarkdown($page);
return $this->download()->directly($pageText, $pageSlug . '.md');
use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\PageRevision;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Repos\RevisionRepo;
use BookStack\Entities\Tools\PageContent;
{
public function __construct(
protected PageRepo $pageRepo,
+ protected PageQueries $pageQueries,
protected RevisionRepo $revisionRepo,
) {
}
*/
public function index(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
'id' => trans('entities.pages_revisions_sort_number')
]);
*/
public function show(string $bookSlug, string $pageSlug, int $revisionId)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
*/
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
*/
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $revisionId);
*/
public function destroy(string $bookSlug, string $pageSlug, int $revId)
{
- $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
*/
public function destroyUserDraft(string $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->revisionRepo->deleteDraftsForCurrentUser($page);
return response('', 200);
namespace BookStack\Entities\Controllers;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
class PageTemplateController extends Controller
{
- protected $pageRepo;
-
- /**
- * PageTemplateController constructor.
- */
- public function __construct(PageRepo $pageRepo)
- {
- $this->pageRepo = $pageRepo;
+ public function __construct(
+ protected PageRepo $pageRepo,
+ protected PageQueries $pageQueries,
+ ) {
}
/**
{
$page = $request->get('page', 1);
$search = $request->get('search', '');
- $templates = $this->pageRepo->getTemplates(10, $page, $search);
+ $count = 10;
+
+ $query = $this->pageQueries->visibleTemplates()
+ ->orderBy('name', 'asc')
+ ->skip(($page - 1) * $count)
+ ->take($count);
+
+ if ($search) {
+ $query->where('name', 'like', '%' . $search . '%');
+ }
+
+ $templates = $query->paginate($count, ['*'], 'page', $page);
+ $templates->withPath('/templates');
if ($search) {
$templates->appends(['search' => $search]);
*/
public function get(int $templateId)
{
- $page = $this->pageRepo->getById($templateId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($templateId);
if (!$page->template) {
throw new NotFoundException();
*
* @throws \Exception
*/
- public function empty()
+ public function empty(TrashCan $trash)
{
- $deleteCount = (new TrashCan())->empty();
+ $deleteCount = $trash->empty();
$this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
/**
* Get the direct child items within this book.
*/
- public function getDirectChildren(): Collection
+ public function getDirectVisibleChildren(): Collection
{
$pages = $this->directPages()->scopes('visible')->get();
$chapters = $this->chapters()->scopes('visible')->get();
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
-
- /**
- * Get a visible book by its slug.
- * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
- */
- public static function getBySlug(string $slug): self
- {
- return static::visible()->where('slug', '=', $slug)->firstOrFail();
- }
}
* @property int $priority
* @property string $book_slug
* @property Book $book
- *
- * @method Builder whereSlugs(string $bookSlug, string $childSlug)
*/
abstract class BookChild extends Entity
{
- protected static function boot()
- {
- parent::boot();
-
- // Load book slugs onto these models by default during query-time
- static::addGlobalScope('book_slug', function (Builder $builder) {
- $builder->addSelect(['book_slug' => function ($builder) {
- $builder->select('slug')
- ->from('books')
- ->whereColumn('books.id', '=', 'book_id');
- }]);
- });
- }
-
- /**
- * Scope a query to find items where the child has the given childSlug
- * where its parent has the bookSlug.
- */
- public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
- {
- return $query->with('book')
- ->whereHas('book', function (Builder $query) use ($bookSlug) {
- $query->where('slug', '=', $bookSlug);
- })
- ->where('slug', '=', $childSlug);
- }
-
/**
* Get the book this page sits in.
*/
* Class Chapter.
*
* @property Collection<Page> $pages
- * @property string $description
* @property ?int $default_template_id
* @property ?Page $defaultTemplate
*/
->orderBy('priority', 'asc')
->get();
}
-
- /**
- * Get a visible chapter by its book and page slugs.
- * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
- */
- public static function getBySlugs(string $bookSlug, string $chapterSlug): self
- {
- return static::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
- }
}
{
use HasFactory;
- public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at', 'priority'];
- public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at', 'priority'];
-
protected $fillable = ['name', 'priority'];
public string $textField = 'text';
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();
- }
}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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');
+ }]));
+ }
+}
--- /dev/null
+<?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;
+ }
+}
+++ /dev/null
-<?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);
- }
-}
--- /dev/null
+<?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');
+ }]);
+ }
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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;
+}
namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\View;
+use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
+use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
-class Popular extends EntityQuery
+class QueryPopular
{
+ public function __construct(
+ protected PermissionApplicator $permissions,
+ protected EntityProvider $entityProvider,
+ ) {
+ }
+
public function run(int $count, int $page, array $filterModels = null)
{
- $query = $this->permissionService()
+ $query = $this->permissions
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModels) {
- $query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
+ $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
$entities = $query->with('viewable')
return $entities;
}
- protected function loadBooksForChildren(Collection $entities)
+ protected function loadBooksForChildren(Collection $entities): void
{
$bookChildren = $entities->filter(fn(Entity $entity) => $entity instanceof BookChild);
$eloquent = (new \Illuminate\Database\Eloquent\Collection($bookChildren));
--- /dev/null
+<?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();
+ }
+}
namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\Favourite;
+use BookStack\Entities\Tools\MixedEntityListLoader;
+use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Query\JoinClause;
-class TopFavourites extends EntityQuery
+class QueryTopFavourites
{
+ public function __construct(
+ protected PermissionApplicator $permissions,
+ protected MixedEntityListLoader $listLoader,
+ ) {
+ }
+
public function run(int $count, int $skip = 0)
{
$user = user();
return collect();
}
- $query = $this->permissionService()
+ $query = $this->permissions
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
->select('favourites.*')
->leftJoin('views', function (JoinClause $join) {
->orderBy('views.views', 'desc')
->where('favourites.user_id', '=', user()->id);
- return $query->with('favouritable')
+ $favourites = $query
->skip($skip)
->take($count)
- ->get()
- ->pluck('favouritable')
- ->filter();
+ ->get();
+
+ $this->listLoader->loadIntoRelations($favourites->all(), 'favouritable', false);
+
+ return $favourites->pluck('favouritable')->filter();
}
}
+++ /dev/null
-<?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();
- }
-}
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\HasHtmlDescription;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException;
use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater;
protected ImageRepo $imageRepo,
protected ReferenceUpdater $referenceUpdater,
protected ReferenceStore $referenceStore,
+ protected PageQueries $pageQueries,
) {
}
return;
}
- $templateExists = Page::query()->visible()
- ->where('template', '=', true)
+ $templateExists = $this->pageQueries->visibleTemplates()
->where('id', '=', $templateId)
->exists();
use BookStack\Activity\ActivityType;
use BookStack\Activity\TagRepo;
use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException;
-use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Uploads\ImageRepo;
use Exception;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
-use Illuminate\Support\Collection;
class BookRepo
{
public function __construct(
protected BaseRepo $baseRepo,
protected TagRepo $tagRepo,
- protected ImageRepo $imageRepo
+ protected ImageRepo $imageRepo,
+ protected TrashCan $trashCan,
) {
}
- /**
- * Get all books in a paginated format.
- */
- public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
- {
- return Book::visible()->with('cover')->orderBy($sort, $order)->paginate($count);
- }
-
- /**
- * Get the books that were most recently viewed by this user.
- */
- public function getRecentlyViewed(int $count = 20): Collection
- {
- return Book::visible()->withLastView()
- ->having('last_viewed_at', '>', 0)
- ->orderBy('last_viewed_at', 'desc')
- ->take($count)->get();
- }
-
- /**
- * Get the most popular books in the system.
- */
- public function getPopular(int $count = 20): Collection
- {
- return Book::visible()->withViewCount()
- ->having('view_count', '>', 0)
- ->orderBy('view_count', 'desc')
- ->take($count)->get();
- }
-
- /**
- * Get the most recently created books from the system.
- */
- public function getRecentlyCreated(int $count = 20): Collection
- {
- return Book::visible()->orderBy('created_at', 'desc')
- ->take($count)->get();
- }
-
- /**
- * Get a book by its slug.
- */
- public function getBySlug(string $slug): Book
- {
- $book = Book::visible()->where('slug', '=', $slug)->first();
-
- if ($book === null) {
- throw new NotFoundException(trans('errors.book_not_found'));
- }
-
- return $book;
- }
-
/**
* Create a new book in the system.
*/
*/
public function destroy(Book $book)
{
- $trashCan = new TrashCan();
- $trashCan->softDestroyBook($book);
+ $this->trashCan->softDestroyBook($book);
Activity::add(ActivityType::BOOK_DELETE, $book);
- $trashCan->autoClearOld();
+ $this->trashCan->autoClearOld();
}
}
namespace BookStack\Entities\Repos;
use BookStack\Activity\ActivityType;
-use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\TrashCan;
-use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use Exception;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
-use Illuminate\Support\Collection;
class BookshelfRepo
{
- protected $baseRepo;
-
- /**
- * BookshelfRepo constructor.
- */
- public function __construct(BaseRepo $baseRepo)
- {
- $this->baseRepo = $baseRepo;
- }
-
- /**
- * Get all bookshelves in a paginated format.
- */
- public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
- {
- return Bookshelf::visible()
- ->with(['visibleBooks', 'cover'])
- ->orderBy($sort, $order)
- ->paginate($count);
- }
-
- /**
- * Get the bookshelves that were most recently viewed by this user.
- */
- public function getRecentlyViewed(int $count = 20): Collection
- {
- return Bookshelf::visible()->withLastView()
- ->having('last_viewed_at', '>', 0)
- ->orderBy('last_viewed_at', 'desc')
- ->take($count)->get();
- }
-
- /**
- * Get the most popular bookshelves in the system.
- */
- public function getPopular(int $count = 20): Collection
- {
- return Bookshelf::visible()->withViewCount()
- ->having('view_count', '>', 0)
- ->orderBy('view_count', 'desc')
- ->take($count)->get();
- }
-
- /**
- * Get the most recently created bookshelves from the system.
- */
- public function getRecentlyCreated(int $count = 20): Collection
- {
- return Bookshelf::visible()->orderBy('created_at', 'desc')
- ->take($count)->get();
- }
-
- /**
- * Get a shelf by its slug.
- */
- public function getBySlug(string $slug): Bookshelf
- {
- $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
-
- if ($shelf === null) {
- throw new NotFoundException(trans('errors.bookshelf_not_found'));
- }
-
- return $shelf;
+ public function __construct(
+ protected BaseRepo $baseRepo,
+ protected BookQueries $bookQueries,
+ protected TrashCan $trashCan,
+ ) {
}
/**
return intval($id);
});
- $syncData = Book::visible()
+ $syncData = $this->bookQueries->visibleForList()
->whereIn('id', $bookIds)
->pluck('id')
->mapWithKeys(function ($bookId) use ($numericIDs) {
*/
public function destroy(Bookshelf $shelf)
{
- $trashCan = new TrashCan();
- $trashCan->softDestroyShelf($shelf);
+ $this->trashCan->softDestroyShelf($shelf);
Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf);
- $trashCan->autoClearOld();
+ $this->trashCan->autoClearOld();
}
}
use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
-use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use Exception;
class ChapterRepo
{
public function __construct(
- protected BaseRepo $baseRepo
+ protected BaseRepo $baseRepo,
+ protected EntityQueries $entityQueries,
+ protected TrashCan $trashCan,
) {
}
- /**
- * Get a chapter via the slug.
- *
- * @throws NotFoundException
- */
- public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
- {
- $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
-
- if ($chapter === null) {
- throw new NotFoundException(trans('errors.chapter_not_found'));
- }
-
- return $chapter;
- }
-
/**
* Create a new chapter in the system.
*/
*/
public function destroy(Chapter $chapter)
{
- $trashCan = new TrashCan();
- $trashCan->softDestroyChapter($chapter);
+ $this->trashCan->softDestroyChapter($chapter);
Activity::add(ActivityType::CHAPTER_DELETE, $chapter);
- $trashCan->autoClearOld();
+ $this->trashCan->autoClearOld();
}
/**
*/
public function move(Chapter $chapter, string $parentIdentifier): Book
{
- $parent = $this->findParentByIdentifier($parentIdentifier);
- if (is_null($parent)) {
+ $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);
+ if (!$parent instanceof Book) {
throw new MoveOperationException('Book to move chapter into not found');
}
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();
- }
}
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
-use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater;
use Exception;
-use Illuminate\Pagination\LengthAwarePaginator;
class PageRepo
{
public function __construct(
protected BaseRepo $baseRepo,
protected RevisionRepo $revisionRepo,
+ protected EntityQueries $entityQueries,
protected ReferenceStore $referenceStore,
- protected ReferenceUpdater $referenceUpdater
+ protected ReferenceUpdater $referenceUpdater,
+ protected TrashCan $trashCan,
) {
}
- /**
- * Get a page by ID.
- *
- * @throws NotFoundException
- */
- public function getById(int $id, array $relations = ['book']): Page
- {
- /** @var Page $page */
- $page = Page::visible()->with($relations)->find($id);
-
- if (!$page) {
- throw new NotFoundException(trans('errors.page_not_found'));
- }
-
- return $page;
- }
-
- /**
- * Get a page its book and own slug.
- *
- * @throws NotFoundException
- */
- public function getBySlug(string $bookSlug, string $pageSlug): Page
- {
- $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
-
- if (!$page) {
- throw new NotFoundException(trans('errors.page_not_found'));
- }
-
- return $page;
- }
-
- /**
- * Get a page by its old slug but checking the revisions table
- * for the last revision that matched the given page and book slug.
- */
- public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
- {
- $revision = $this->revisionRepo->getBySlugs($bookSlug, $pageSlug);
-
- return $revision->page ?? null;
- }
-
- /**
- * Get pages that have been marked as a template.
- */
- public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
- {
- $query = Page::visible()
- ->where('template', '=', true)
- ->orderBy('name', 'asc')
- ->skip(($page - 1) * $count)
- ->take($count);
-
- if ($search) {
- $query->where('name', 'like', '%' . $search . '%');
- }
-
- $paginator = $query->paginate($count, ['*'], 'page', $page);
- $paginator->withPath('/templates');
-
- return $paginator;
- }
-
- /**
- * Get a parent item via slugs.
- */
- public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
- {
- if ($chapterSlug !== null) {
- return Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
- }
-
- return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
- }
-
- /**
- * Get the draft copy of the given page for the current user.
- */
- public function getUserDraft(Page $page): ?PageRevision
- {
- return $this->revisionRepo->getLatestDraftForCurrentUser($page);
- }
-
/**
* Get a new draft page belonging to the given parent entity.
*/
*/
public function destroy(Page $page)
{
- $trashCan = new TrashCan();
- $trashCan->softDestroyPage($page);
+ $this->trashCan->softDestroyPage($page);
Activity::add(ActivityType::PAGE_DELETE, $page);
- $trashCan->autoClearOld();
+ $this->trashCan->autoClearOld();
}
/**
*/
public function move(Page $page, string $parentIdentifier): Entity
{
- $parent = $this->findParentByIdentifier($parentIdentifier);
- if (is_null($parent)) {
+ $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier);
+ if (!$parent instanceof Chapter && !$parent instanceof Book) {
throw new MoveOperationException('Book or chapter to move page into not found');
}
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.
*/
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
-use Illuminate\Database\Eloquent\Builder;
+use BookStack\Entities\Queries\PageRevisionQueries;
class RevisionRepo
{
- /**
- * Get a revision by its stored book and page slug values.
- */
- public function getBySlugs(string $bookSlug, string $pageSlug): ?PageRevision
- {
- /** @var ?PageRevision $revision */
- $revision = PageRevision::query()
- ->whereHas('page', function (Builder $query) {
- $query->scopes('visible');
- })
- ->where('slug', '=', $pageSlug)
- ->where('type', '=', 'version')
- ->where('book_slug', '=', $bookSlug)
- ->orderBy('created_at', 'desc')
- ->with('page')
- ->first();
-
- return $revision;
- }
-
- /**
- * Get the latest draft revision, for the given page, belonging to the current user.
- */
- public function getLatestDraftForCurrentUser(Page $page): ?PageRevision
- {
- /** @var ?PageRevision $revision */
- $revision = $this->queryForCurrentUserDraft($page->id)->first();
-
- return $revision;
+ public function __construct(
+ protected PageRevisionQueries $queries,
+ ) {
}
/**
*/
public function deleteDraftsForCurrentUser(Page $page): void
{
- $this->queryForCurrentUserDraft($page->id)->delete();
+ $this->queries->latestCurrentUserDraftsForPageId($page->id)->delete();
}
/**
*/
public function getNewDraftForCurrentUser(Page $page): PageRevision
{
- $draft = $this->getLatestDraftForCurrentUser($page);
+ $draft = $this->queries->findLatestCurrentUserDraftsForPageId($page->id);
if ($draft) {
return $draft;
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');
- }
}
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use Illuminate\Support\Collection;
class BookContents
{
- protected Book $book;
+ protected EntityQueries $queries;
- public function __construct(Book $book)
- {
- $this->book = $book;
+ public function __construct(
+ protected Book $book,
+ ) {
+ $this->queries = app()->make(EntityQueries::class);
}
/**
*/
public function getLastPriority(): int
{
- $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
+ $maxPage = $this->book->pages()
->where('draft', '=', false)
- ->where('chapter_id', '=', 0)->max('priority');
- $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
+ ->where('chapter_id', '=', 0)
+ ->max('priority');
+
+ $maxChapter = $this->book->chapters()
->max('priority');
return max($maxChapter, $maxPage, 1);
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
{
$pages = $this->getPages($showDrafts, $renderPages);
- $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
+ $chapters = $this->book->chapters()->scopes('visible')->get();
$all = collect()->concat($pages)->concat($chapters);
$chapterMap = $chapters->keyBy('id');
$lonePages = collect();
*/
protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
{
- $query = Page::visible()
- ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
- ->where('book_id', '=', $this->book->id);
+ if ($getPageContent) {
+ $query = $this->queries->pages->visibleWithContents();
+ } else {
+ $query = $this->queries->pages->visibleForList();
+ }
if (!$showDrafts) {
$query->where('draft', '=', false);
}
- return $query->get();
+ return $query->where('book_id', '=', $this->book->id)->get();
}
/**
/** @var Book[] $booksInvolved */
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
- return strpos($key, 'book:') === 0;
+ return str_starts_with($key, 'book:');
}, ARRAY_FILTER_USE_KEY));
// Update permissions of books involved
}
}
- $pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes);
+ $pages = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();
/** @var Page $page */
foreach ($pages as $page) {
$modelMap['page:' . $page->id] = $page;
}
}
- $chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get();
+ $chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();
/** @var Chapter $chapter */
foreach ($chapters as $chapter) {
$modelMap['chapter:' . $chapter->id] = $chapter;
$ids['book'][] = $chapter->book_id;
}
- $books = Book::visible()->whereIn('id', array_unique($ids['book']))->get();
+ $books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();
/** @var Book $book */
foreach ($books as $book) {
$modelMap['book:' . $book->id] = $book;
$copyBook = $this->bookRepo->create($bookDetails);
// Clone contents
- $directChildren = $original->getDirectChildren();
+ $directChildren = $original->getDirectVisibleChildren();
foreach ($directChildren as $child) {
if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) {
$this->cloneChapter($child, $copyBook, $child->name);
namespace BookStack\Entities\Tools;
use BookStack\App\Model;
-use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Queries\EntityQueries;
use Illuminate\Database\Eloquent\Relations\Relation;
class MixedEntityListLoader
{
- protected array $listAttributes = [
- 'page' => ['id', 'name', 'slug', 'book_id', 'chapter_id', 'text', 'draft'],
- 'chapter' => ['id', 'name', 'slug', 'book_id', 'description'],
- 'book' => ['id', 'name', 'slug', 'description'],
- 'bookshelf' => ['id', 'name', 'slug', 'description'],
- ];
-
public function __construct(
- protected EntityProvider $entityProvider
+ protected EntityQueries $queries,
) {
}
* This will look for a model id and type via 'name_id' and 'name_type'.
* @param Model[] $relations
*/
- public function loadIntoRelations(array $relations, string $relationName): void
+ public function loadIntoRelations(array $relations, string $relationName, bool $loadParents): void
{
$idsByType = [];
foreach ($relations as $relation) {
$idsByType[$type][] = $id;
}
- $modelMap = $this->idsByTypeToModelMap($idsByType);
+ $modelMap = $this->idsByTypeToModelMap($idsByType, $loadParents);
foreach ($relations as $relation) {
$type = $relation->getAttribute($relationName . '_type');
* @param array<string, int[]> $idsByType
* @return array<string, array<int, Model>>
*/
- protected function idsByTypeToModelMap(array $idsByType): array
+ protected function idsByTypeToModelMap(array $idsByType, bool $eagerLoadParents): array
{
$modelMap = [];
foreach ($idsByType as $type => $ids) {
- if (!isset($this->listAttributes[$type])) {
- continue;
- }
-
- $instance = $this->entityProvider->get($type);
- $models = $instance->newQuery()
- ->select($this->listAttributes[$type])
- ->scopes('visible')
+ $models = $this->queries->visibleForList($type)
->whereIn('id', $ids)
- ->with($this->getRelationsToEagerLoad($type))
+ ->with($eagerLoadParents ? $this->getRelationsToEagerLoad($type) : [])
->get();
if (count($models) > 0) {
namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme;
class PageContent
{
+ protected PageQueries $pageQueries;
+
public function __construct(
protected Page $page
) {
+ $this->pageQueries = app()->make(PageQueries::class);
}
/**
protected function getContentProviderClosure(bool $blankIncludes): Closure
{
$contextPage = $this->page;
+ $queries = $this->pageQueries;
- return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage): PageIncludeContent {
+ return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage, $queries): PageIncludeContent {
if ($blankIncludes) {
return PageIncludeContent::fromHtmlAndTag('', $tag);
}
- $matchedPage = Page::visible()->find($tag->getPageId());
+ $matchedPage = $queries->findVisibleById($tag->getPageId());
$content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag);
if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) {
use BookStack\Activity\Tools\CommentTree;
use BookStack\Entities\Models\Page;
-use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
public function __construct(
protected Page $page,
- protected PageRepo $pageRepo,
+ protected EntityQueries $queries,
protected string $requestedEditor
) {
$this->viewData = $this->build();
{
$page = clone $this->page;
$isDraft = boolval($this->page->draft);
- $templates = $this->pageRepo->getTemplates(10);
+ $templates = $this->queries->pages->visibleTemplates()
+ ->orderBy('name', 'asc')
+ ->take(10)
+ ->paginate()
+ ->withPath('/templates');
+
$draftsEnabled = auth()->check();
$isDraftRevision = false;
}
// Check for a current draft version for this user
- $userDraft = $this->pageRepo->getUserDraft($page);
- if ($userDraft !== null) {
+ $userDraft = $this->queries->revisions->findLatestCurrentUserDraftsForPageId($page->id);
+ if (!is_null($userDraft)) {
$page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$isDraftRevision = true;
$this->warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
class ShelfContext
{
- protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
+ protected string $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
+
+ public function __construct(
+ protected BookshelfQueries $shelfQueries,
+ ) {
+ }
/**
* Get the current bookshelf context for the given book.
return null;
}
- /** @var Bookshelf $shelf */
- $shelf = Bookshelf::visible()->find($contextBookshelfId);
+ $shelf = $this->shelfQueries->findVisibleById($contextBookshelfId);
$shelfContainsBook = $shelf && $shelf->contains($book);
return $shelfContainsBook ? $shelf : null;
/**
* 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);
}
/**
* Clear the session stored shelf context id.
*/
- public function clearShelfContext()
+ public function clearShelfContext(): void
{
session()->forget($this->KEY_SHELF_CONTEXT_ID);
}
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use Illuminate\Support\Collection;
class SiblingFetcher
{
+ public function __construct(
+ protected EntityQueries $queries,
+ protected ShelfContext $shelfContext,
+ ) {
+ }
+
/**
* Search among the siblings of the entity of given type and id.
*/
// Page in book or chapter
if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) {
- $entities = $entity->book->getDirectChildren();
+ $entities = $entity->book->getDirectVisibleChildren();
}
// Book
// Gets just the books in a shelf if shelf is in context
if ($entity instanceof Book) {
- $contextShelf = (new ShelfContext())->getContextualShelfForBook($entity);
+ $contextShelf = $this->shelfContext->getContextualShelfForBook($entity);
if ($contextShelf) {
$entities = $contextShelf->visibleBooks()->get();
} else {
- $entities = Book::visible()->get();
+ $entities = $this->queries->books->visibleForList()->get();
}
}
// Shelf
if ($entity instanceof Bookshelf) {
- $entities = Bookshelf::visible()->get();
+ $entities = $this->queries->shelves->visibleForList()->get();
}
return $entities;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use BookStack\Uploads\AttachmentService;
class TrashCan
{
+ public function __construct(
+ protected EntityQueries $queries,
+ ) {
+ }
+
/**
* Send a shelf to the recycle bin.
*
}
// Remove book template usages
- Book::query()->where('default_template_id', '=', $page->id)
+ $this->queries->books->start()
+ ->where('default_template_id', '=', $page->id)
->update(['default_template_id' => null]);
// Remove chapter template usages
- Chapter::query()->where('default_template_id', '=', $page->id)
+ $this->queries->chapters->start()
+ ->where('default_template_id', '=', $page->id)
->update(['default_template_id' => null]);
$page->forceDelete();
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
-use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Users\Models\Role;
use Illuminate\Database\Eloquent\Builder;
*/
class JointPermissionBuilder
{
+ public function __construct(
+ protected EntityQueries $queries,
+ ) {
+ }
+
+
/**
* Re-generate all entity permission from scratch.
*/
});
// Chunk through all bookshelves
- Bookshelf::query()->withTrashed()->select(['id', 'owned_by'])
+ $this->queries->shelves->start()->withTrashed()->select(['id', 'owned_by'])
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
});
// Chunk through all bookshelves
- Bookshelf::query()->select(['id', 'owned_by'])
+ $this->queries->shelves->start()->select(['id', 'owned_by'])
->chunk(100, function ($shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
*/
protected function bookFetchQuery(): Builder
{
- return Book::query()->withTrashed()
+ return $this->queries->books->start()->withTrashed()
->select(['id', 'owned_by'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id']);
namespace BookStack\Permissions;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Http\Controller;
use BookStack\Permissions\Models\EntityPermission;
class PermissionsController extends Controller
{
- protected PermissionsUpdater $permissionsUpdater;
-
- public function __construct(PermissionsUpdater $permissionsUpdater)
- {
- $this->permissionsUpdater = $permissionsUpdater;
+ public function __construct(
+ protected PermissionsUpdater $permissionsUpdater,
+ protected EntityQueries $queries,
+ ) {
}
/**
- * Show the Permissions view for a page.
+ * Show the permissions view for a page.
*/
public function showForPage(string $bookSlug, string $pageSlug)
{
- $page = Page::getBySlugs($bookSlug, $pageSlug);
+ $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->setPageTitle(trans('entities.pages_permissions'));
*/
public function updateForPage(Request $request, string $bookSlug, string $pageSlug)
{
- $page = Page::getBySlugs($bookSlug, $pageSlug);
+ $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->permissionsUpdater->updateFromPermissionsForm($page, $request);
}
/**
- * Show the Restrictions view for a chapter.
+ * Show the permissions view for a chapter.
*/
public function showForChapter(string $bookSlug, string $chapterSlug)
{
- $chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
+ $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->setPageTitle(trans('entities.chapters_permissions'));
}
/**
- * Set the restrictions for a chapter.
+ * Set the permissions for a chapter.
*/
public function updateForChapter(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
+ $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
*/
public function showForBook(string $slug)
{
- $book = Book::getBySlug($slug);
+ $book = $this->queries->books->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->setPageTitle(trans('entities.books_permissions'));
}
/**
- * Set the restrictions for a book.
+ * Set the permissions for a book.
*/
public function updateForBook(Request $request, string $slug)
{
- $book = Book::getBySlug($slug);
+ $book = $this->queries->books->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->permissionsUpdater->updateFromPermissionsForm($book, $request);
*/
public function showForShelf(string $slug)
{
- $shelf = Bookshelf::getBySlug($slug);
+ $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$this->setPageTitle(trans('entities.shelves_permissions'));
*/
public function updateForShelf(Request $request, string $slug)
{
- $shelf = Bookshelf::getBySlug($slug);
+ $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
*/
public function copyShelfPermissionsToBooks(string $slug)
{
- $shelf = Bookshelf::getBySlug($slug);
+ $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
namespace BookStack\References;
use BookStack\App\Model;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\References\ModelResolvers\BookLinkModelResolver;
use BookStack\References\ModelResolvers\BookshelfLinkModelResolver;
use BookStack\References\ModelResolvers\ChapterLinkModelResolver;
*/
public static function createWithEntityResolvers(): self
{
+ $queries = app()->make(EntityQueries::class);
+
return new self([
- new PagePermalinkModelResolver(),
- new PageLinkModelResolver(),
- new ChapterLinkModelResolver(),
- new BookLinkModelResolver(),
- new BookshelfLinkModelResolver(),
+ new PagePermalinkModelResolver($queries->pages),
+ new PageLinkModelResolver($queries->pages),
+ new ChapterLinkModelResolver($queries->chapters),
+ new BookLinkModelResolver($queries->books),
+ new BookshelfLinkModelResolver($queries->shelves),
]);
}
}
use BookStack\App\Model;
use BookStack\Entities\Models\Book;
+use BookStack\Entities\Queries\BookQueries;
class BookLinkModelResolver implements CrossLinkModelResolver
{
+ public function __construct(
+ protected BookQueries $queries
+ ) {
+ }
+
public function resolve(string $link): ?Model
{
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
$bookSlug = $matches[1];
/** @var ?Book $model */
- $model = Book::query()->where('slug', '=', $bookSlug)->first(['id']);
+ $model = $this->queries->start()->where('slug', '=', $bookSlug)->first(['id']);
return $model;
}
use BookStack\App\Model;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Queries\BookshelfQueries;
class BookshelfLinkModelResolver implements CrossLinkModelResolver
{
+ public function __construct(
+ protected BookshelfQueries $queries
+ ) {
+ }
public function resolve(string $link): ?Model
{
$pattern = '/^' . preg_quote(url('/shelves'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
$shelfSlug = $matches[1];
/** @var ?Bookshelf $model */
- $model = Bookshelf::query()->where('slug', '=', $shelfSlug)->first(['id']);
+ $model = $this->queries->start()->where('slug', '=', $shelfSlug)->first(['id']);
return $model;
}
use BookStack\App\Model;
use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Queries\ChapterQueries;
class ChapterLinkModelResolver implements CrossLinkModelResolver
{
+ public function __construct(
+ protected ChapterQueries $queries
+ ) {
+ }
+
public function resolve(string $link): ?Model
{
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/chapter\/' . '([\w-]+)' . '([#?\/]|$)/';
$chapterSlug = $matches[2];
/** @var ?Chapter $model */
- $model = Chapter::query()->whereSlugs($bookSlug, $chapterSlug)->first(['id']);
+ $model = $this->queries->usingSlugs($bookSlug, $chapterSlug)->first(['id']);
return $model;
}
use BookStack\App\Model;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
class PageLinkModelResolver implements CrossLinkModelResolver
{
+ public function __construct(
+ protected PageQueries $queries
+ ) {
+ }
+
public function resolve(string $link): ?Model
{
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/page\/' . '([\w-]+)' . '([#?\/]|$)/';
$pageSlug = $matches[2];
/** @var ?Page $model */
- $model = Page::query()->whereSlugs($bookSlug, $pageSlug)->first(['id']);
+ $model = $this->queries->usingSlugs($bookSlug, $pageSlug)->first(['id']);
return $model;
}
use BookStack\App\Model;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
class PagePermalinkModelResolver implements CrossLinkModelResolver
{
+ public function __construct(
+ protected PageQueries $queries
+ ) {
+ }
+
public function resolve(string $link): ?Model
{
$pattern = '/^' . preg_quote(url('/link'), '/') . '\/(\d+)/';
$id = intval($matches[1]);
/** @var ?Page $model */
- $model = Page::query()->find($id, ['id']);
+ $model = $this->queries->start()->find($id, ['id']);
return $model;
}
namespace BookStack\References;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Http\Controller;
class ReferenceController extends Controller
{
public function __construct(
- protected ReferenceFetcher $referenceFetcher
+ protected ReferenceFetcher $referenceFetcher,
+ protected EntityQueries $queries,
) {
}
*/
public function page(string $bookSlug, string $pageSlug)
{
- $page = Page::getBySlugs($bookSlug, $pageSlug);
+ $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$references = $this->referenceFetcher->getReferencesToEntity($page);
return view('pages.references', [
*/
public function chapter(string $bookSlug, string $chapterSlug)
{
- $chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
+ $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$references = $this->referenceFetcher->getReferencesToEntity($chapter);
return view('chapters.references', [
*/
public function book(string $slug)
{
- $book = Book::getBySlug($slug);
+ $book = $this->queries->books->findVisibleBySlugOrFail($slug);
$references = $this->referenceFetcher->getReferencesToEntity($book);
return view('books.references', [
*/
public function shelf(string $slug)
{
- $shelf = Bookshelf::getBySlug($slug);
+ $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$references = $this->referenceFetcher->getReferencesToEntity($shelf);
return view('shelves.references', [
public function getReferencesToEntity(Entity $entity): Collection
{
$references = $this->queryReferencesToEntity($entity)->get();
- $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from');
+ $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', true);
return $references;
}
namespace BookStack\Search;
-use BookStack\Entities\Models\Page;
-use BookStack\Entities\Queries\Popular;
+use BookStack\Entities\Queries\PageQueries;
+use BookStack\Entities\Queries\QueryPopular;
use BookStack\Entities\Tools\SiblingFetcher;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function __construct(
- protected SearchRunner $searchRunner
+ protected SearchRunner $searchRunner,
+ protected PageQueries $pageQueries,
) {
}
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
*/
- public function searchForSelector(Request $request)
+ public function searchForSelector(Request $request, QueryPopular $queryPopular)
{
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false);
$searchTerm .= ' {type:' . implode('|', $entityTypes) . '}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20)['results'];
} else {
- $entities = (new Popular())->run(20, 0, $entityTypes);
+ $entities = $queryPopular->run(20, 0, $entityTypes);
}
return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);
$searchOptions->setFilter('is_template');
$entities = $this->searchRunner->searchEntities($searchOptions, 'page', 1, 20)['results'];
} else {
- $entities = Page::visible()
- ->where('template', '=', true)
+ $entities = $this->pageQueries->visibleTemplates()
->where('draft', '=', false)
->orderBy('updated_at', 'desc')
->take(20)
- ->get(Page::$listAttributes);
+ ->get();
}
return view('search.parts.entity-selector-list', [
/**
* Search siblings items in the system.
*/
- public function searchSiblings(Request $request)
+ public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
- $entities = (new SiblingFetcher())->fetch($type, $id);
+ $entities = $siblingFetcher->fetch($type, $id);
return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
}
namespace BookStack\Search;
use BookStack\Entities\EntityProvider;
-use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Database\Connection;
class SearchRunner
{
- protected EntityProvider $entityProvider;
- protected PermissionApplicator $permissions;
-
/**
* Acceptable operators to be used in a query.
*
*/
protected $termAdjustmentCache;
- public function __construct(EntityProvider $entityProvider, PermissionApplicator $permissions)
- {
- $this->entityProvider = $entityProvider;
- $this->permissions = $permissions;
+ public function __construct(
+ protected EntityProvider $entityProvider,
+ protected PermissionApplicator $permissions,
+ protected EntityQueries $entityQueries,
+ ) {
$this->termAdjustmentCache = new SplObjectStorage();
}
continue;
}
- $entityModelInstance = $this->entityProvider->get($entityType);
- $searchQuery = $this->buildQuery($searchOpts, $entityModelInstance);
+ $searchQuery = $this->buildQuery($searchOpts, $entityType);
$entityTotal = $searchQuery->count();
- $searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityModelInstance, $page, $count);
+ $searchResults = $this->getPageOfDataFromQuery($searchQuery, $entityType, $page, $count);
if ($entityTotal > ($page * $count)) {
$hasMore = true;
continue;
}
- $entityModelInstance = $this->entityProvider->get($entityType);
- $search = $this->buildQuery($opts, $entityModelInstance)->where('book_id', '=', $bookId)->take(20)->get();
+ $search = $this->buildQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
public function searchChapter(int $chapterId, string $searchString): Collection
{
$opts = SearchOptions::fromString($searchString);
- $entityModelInstance = $this->entityProvider->get('page');
- $pages = $this->buildQuery($opts, $entityModelInstance)->where('chapter_id', '=', $chapterId)->take(20)->get();
+ $pages = $this->buildQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score');
}
/**
* Get a page of result data from the given query based on the provided page parameters.
*/
- protected function getPageOfDataFromQuery(EloquentBuilder $query, Entity $entityModelInstance, int $page = 1, int $count = 20): EloquentCollection
+ protected function getPageOfDataFromQuery(EloquentBuilder $query, string $entityType, int $page = 1, int $count = 20): EloquentCollection
{
$relations = ['tags'];
- if ($entityModelInstance instanceof BookChild) {
+ if ($entityType === 'page' || $entityType === 'chapter') {
$relations['book'] = function (BelongsTo $query) {
$query->scopes('visible');
};
}
- if ($entityModelInstance instanceof Page) {
+ if ($entityType === 'page') {
$relations['chapter'] = function (BelongsTo $query) {
$query->scopes('visible');
};
/**
* Create a search query for an entity.
*/
- protected function buildQuery(SearchOptions $searchOpts, Entity $entityModelInstance): EloquentBuilder
+ protected function buildQuery(SearchOptions $searchOpts, string $entityType): EloquentBuilder
{
- $entityQuery = $entityModelInstance->newQuery()->scopes('visible');
-
- if ($entityModelInstance instanceof Page) {
- $entityQuery->select(array_merge($entityModelInstance::$listAttributes, ['owned_by']));
- } else {
- $entityQuery->select(['*']);
- }
+ $entityModelInstance = $this->entityProvider->get($entityType);
+ $entityQuery = $this->entityQueries->visibleForList($entityType);
// Handle normal search terms
- $this->applyTermSearch($entityQuery, $searchOpts, $entityModelInstance);
+ $this->applyTermSearch($entityQuery, $searchOpts, $entityType);
// Handle exact term matching
foreach ($searchOpts->exacts as $inputTerm) {
/**
* For the given search query, apply the queries for handling the regular search terms.
*/
- protected function applyTermSearch(EloquentBuilder $entityQuery, SearchOptions $options, Entity $entity): void
+ protected function applyTermSearch(EloquentBuilder $entityQuery, SearchOptions $options, string $entityType): void
{
$terms = $options->searches;
if (count($terms) === 0) {
$subQuery->addBinding($scoreSelect['bindings'], 'select');
- $subQuery->where('entity_type', '=', $entity->getMorphClass());
+ $subQuery->where('entity_type', '=', $entityType);
$subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms as $inputTerm) {
$inputTerm = str_replace('\\', '\\\\', $inputTerm);
/**
* Show the page for application maintenance.
*/
- public function index()
+ public function index(TrashCan $trashCan)
{
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint'));
$version = trim(file_get_contents(base_path('version')));
// Recycle bin details
- $recycleStats = (new TrashCan())->getTrashedCounts();
+ $recycleStats = $trashCan->getTrashedCounts();
return view('settings.maintenance', [
'version' => $version,
use BookStack\Exceptions\FileUploadException;
use Exception;
-use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log;
namespace BookStack\Uploads\Controllers;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\FileUploadException;
use BookStack\Http\ApiController;
use BookStack\Uploads\Attachment;
class AttachmentApiController extends ApiController
{
public function __construct(
- protected AttachmentService $attachmentService
+ protected AttachmentService $attachmentService,
+ protected PageQueries $pageQueries,
) {
}
$requestData = $this->validate($request, $this->rules()['create']);
$pageId = $request->get('uploaded_to');
- $page = Page::visible()->findOrFail($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
if ($request->hasFile('file')) {
$page = $attachment->page;
if ($requestData['uploaded_to'] ?? false) {
$pageId = $request->get('uploaded_to');
- $page = Page::visible()->findOrFail($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$attachment->uploaded_to = $requestData['uploaded_to'];
}
namespace BookStack\Uploads\Controllers;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\FileUploadException;
use BookStack\Exceptions\NotFoundException;
{
public function __construct(
protected AttachmentService $attachmentService,
+ protected PageQueries $pageQueries,
protected PageRepo $pageRepo
) {
}
]);
$pageId = $request->get('uploaded_to');
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
]), 422);
}
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
*/
public function listForPage(int $pageId)
{
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-view', $page);
return view('attachments.manager-list', [
$this->validate($request, [
'order' => ['required', 'array'],
]);
- $page = $this->pageRepo->getById($pageId);
+ $page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
$attachmentOrder = $request->get('order');
$attachment = Attachment::query()->findOrFail($attachmentId);
try {
- $page = $this->pageRepo->getById($attachment->uploaded_to);
+ $page = $this->pageQueries->findVisibleByIdOrFail($attachment->uploaded_to);
} catch (NotFoundException $exception) {
throw new NotFoundException(trans('errors.attachment_not_found'));
}
use BookStack\Uploads\ImageResizer;
use BookStack\Util\OutOfMemoryHandler;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\App;
-use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller
namespace BookStack\Uploads\Controllers;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\ApiController;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
public function __construct(
protected ImageRepo $imageRepo,
protected ImageResizer $imageResizer,
+ protected PageQueries $pageQueries,
) {
}
{
$this->checkPermission('image-create-all');
$data = $this->validate($request, $this->rules()['create']);
- Page::visible()->findOrFail($data['uploaded_to']);
+ $page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']);
- $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']);
+ $image = $this->imageRepo->saveNew($data['image'], $data['type'], $page->id);
if (isset($data['name'])) {
$image->refresh();
namespace BookStack\Uploads;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Permissions\PermissionApplicator;
use Exception;
protected ImageService $imageService,
protected PermissionApplicator $permissions,
protected ImageResizer $imageResizer,
+ protected PageQueries $pageQueries,
) {
}
*/
public function getEntityFiltered(
string $type,
- string $filterType = null,
- int $page = 0,
- int $pageSize = 24,
- int $uploadedTo = null,
- string $search = null
+ ?string $filterType,
+ int $page,
+ int $pageSize,
+ int $uploadedTo,
+ ?string $search
): array {
- /** @var Page $contextPage */
- $contextPage = Page::visible()->findOrFail($uploadedTo);
+ $contextPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo);
$parentFilter = null;
if ($filterType === 'book' || $filterType === 'page') {
*/
public function getPagesUsingImage(Image $image): array
{
- $pages = Page::visible()
+ $pages = $this->pageQueries->visibleForList()
->where('html', 'like', '%' . $image->url . '%')
- ->get(['id', 'name', 'slug', 'book_id']);
+ ->get();
foreach ($pages as $page) {
$page->setAttribute('url', $page->getUrl());
namespace BookStack\Uploads;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\ImageUploadException;
use Exception;
use Illuminate\Support\Facades\DB;
public function __construct(
protected ImageStorage $storage,
protected ImageResizer $resizer,
+ protected EntityQueries $queries,
) {
}
}
if ($imageType === 'gallery' || $imageType === 'drawio') {
- return Page::visible()->where('id', '=', $image->uploaded_to)->exists();
+ return $this->queries->pages->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
}
if ($imageType === 'cover_book') {
- return Book::visible()->where('id', '=', $image->uploaded_to)->exists();
+ return $this->queries->books->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
}
if ($imageType === 'cover_bookshelf') {
- return Bookshelf::visible()->where('id', '=', $image->uploaded_to)->exists();
+ return $this->queries->shelves->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
}
return false;
class UserProfileController extends Controller
{
+ public function __construct(
+ protected UserRepo $userRepo,
+ protected ActivityQueries $activityQueries,
+ protected UserContentCounts $contentCounts,
+ protected UserRecentlyCreatedContent $recentlyCreatedContent
+ ) {
+ }
+
+
/**
* Show the user profile page.
*/
- public function show(UserRepo $repo, ActivityQueries $activities, string $slug)
+ public function show(string $slug)
{
- $user = $repo->getBySlug($slug);
+ $user = $this->userRepo->getBySlug($slug);
- $userActivity = $activities->userActivity($user);
- $recentlyCreated = (new UserRecentlyCreatedContent())->run($user, 5);
- $assetCounts = (new UserContentCounts())->run($user);
+ $userActivity = $this->activityQueries->userActivity($user);
+ $recentlyCreated = $this->recentlyCreatedContent->run($user, 5);
+ $assetCounts = $this->contentCounts->run($user);
$this->setPageTitle($user->name);
namespace BookStack\Users\Queries;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Users\Models\User;
/**
*/
class UserContentCounts
{
+ public function __construct(
+ protected EntityQueries $queries,
+ ) {
+ }
+
+
/**
* @return array{pages: int, chapters: int, books: int, shelves: int}
*/
$createdBy = ['created_by' => $user->id];
return [
- 'pages' => Page::visible()->where($createdBy)->count(),
- 'chapters' => Chapter::visible()->where($createdBy)->count(),
- 'books' => Book::visible()->where($createdBy)->count(),
- 'shelves' => Bookshelf::visible()->where($createdBy)->count(),
+ 'pages' => $this->queries->pages->visibleForList()->where($createdBy)->count(),
+ 'chapters' => $this->queries->chapters->visibleForList()->where($createdBy)->count(),
+ 'books' => $this->queries->books->visibleForList()->where($createdBy)->count(),
+ 'shelves' => $this->queries->shelves->visibleForList()->where($createdBy)->count(),
];
}
}
namespace BookStack\Users\Queries;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
*/
class UserRecentlyCreatedContent
{
+ public function __construct(
+ protected EntityQueries $queries,
+ ) {
+ }
+
/**
* @return array{pages: Collection, chapters: Collection, books: Collection, shelves: Collection}
*/
};
return [
- 'pages' => $query(Page::visible()->where('draft', '=', false)),
- 'chapters' => $query(Chapter::visible()),
- 'books' => $query(Book::visible()),
- 'shelves' => $query(Bookshelf::visible()),
+ 'pages' => $query($this->queries->pages->visibleForList()->where('draft', '=', false)),
+ 'chapters' => $query($this->queries->chapters->visibleForList()),
+ 'books' => $query($this->queries->books->visibleForList()),
+ 'shelves' => $query($this->queries->shelves->visibleForList()),
];
}
}
--- /dev/null
+<?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');
+ });
+ }
+};
@extends('layouts.simple')
-
+@inject('popular', \BookStack\Entities\Queries\QueryPopular::class)
@section('content')
<div class="container mt-l">
<div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
<div class="px-m">
- @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
+ @include('entities.list', ['entities' => $popular->run(10, 0, ['page']), 'style' => 'compact'])
</div>
</div>
</div>
<div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
<div class="px-m">
- @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
+ @include('entities.list', ['entities' => $popular->run(10, 0, ['book']), 'style' => 'compact'])
</div>
</div>
</div>
<div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
<div class="px-m">
- @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
+ @include('entities.list', ['entities' => $popular->run(10, 0, ['chapter']), 'style' => 'compact'])
</div>
</div>
</div>
'id' => $firstBook->id,
'name' => $firstBook->name,
'slug' => $firstBook->slug,
+ 'owned_by' => $firstBook->owned_by,
+ 'created_by' => $firstBook->created_by,
+ 'updated_by' => $firstBook->updated_by,
],
]]);
}
'book_id' => $firstChapter->book->id,
'priority' => $firstChapter->priority,
'book_slug' => $firstChapter->book->slug,
+ 'owned_by' => $firstChapter->owned_by,
+ 'created_by' => $firstChapter->created_by,
+ 'updated_by' => $firstChapter->updated_by,
],
]]);
}
'id' => $page->id,
'slug' => $page->slug,
'name' => $page->name,
+ 'owned_by' => $page->owned_by,
+ 'created_by' => $page->created_by,
+ 'updated_by' => $page->updated_by,
+ 'book_id' => $page->id,
+ 'chapter_id' => $chapter->id,
+ 'priority' => $page->priority,
+ 'book_slug' => $chapter->book->slug,
+ 'draft' => $page->draft,
+ 'template' => $page->template,
+ 'editor' => $page->editor,
],
],
'default_template_id' => null,
'slug' => $firstPage->slug,
'book_id' => $firstPage->book->id,
'priority' => $firstPage->priority,
+ 'owned_by' => $firstPage->owned_by,
+ 'created_by' => $firstPage->created_by,
+ 'updated_by' => $firstPage->updated_by,
+ 'revision_count' => $firstPage->revision_count,
],
]]);
}
'id' => $firstBookshelf->id,
'name' => $firstBookshelf->name,
'slug' => $firstBookshelf->slug,
+ 'owned_by' => $firstBookshelf->owned_by,
+ 'created_by' => $firstBookshelf->created_by,
+ 'updated_by' => $firstBookshelf->updated_by,
],
]]);
}
$copy = Book::query()->where('name', '=', 'My copy book')->first();
$resp->assertRedirect($copy->getUrl());
- $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
+ $this->assertEquals($book->getDirectVisibleChildren()->count(), $copy->getDirectVisibleChildren()->count());
$this->get($copy->getUrl())->assertSee($book->description_html, false);
}
// Hide child content
/** @var BookChild $page */
- foreach ($book->getDirectChildren() as $child) {
+ foreach ($book->getDirectVisibleChildren() as $child) {
$this->permissions->setEntityPermissions($child, [], []);
}
/** @var Book $copy */
$copy = Book::query()->where('name', '=', 'My copy book')->first();
- $this->assertEquals(0, $copy->getDirectChildren()->count());
+ $this->assertEquals(0, $copy->getDirectVisibleChildren()->count());
}
public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
public function test_sibling_search_for_pages_without_chapter()
{
$page = $this->entities->pageNotWithinChapter();
- $bookChildren = $page->book->getDirectChildren();
+ $bookChildren = $page->book->getDirectVisibleChildren();
$this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
$search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
public function test_sibling_search_for_chapters()
{
$chapter = $this->entities->chapter();
- $bookChildren = $chapter->book->getDirectChildren();
+ $bookChildren = $chapter->book->getDirectVisibleChildren();
$this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
$search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter");