Compare commits

...

3 Commits

Author SHA1 Message Date
Dan Brown
fa3b7f77b9 Maintenance: Addressed all phpstan level 5 issues 2026-05-31 15:42:54 +01:00
Dan Brown
03c9ddd14f Maintenance: Further PHPStan level 5 type fixes 2026-05-31 11:12:23 +01:00
Dan Brown
5611c8c0da Maintenance: Started work on PHPStan Level 5 2026-05-30 21:44:25 +01:00
39 changed files with 149 additions and 63 deletions

View File

@@ -42,7 +42,7 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Retrieve a user by the given credentials. * Retrieve a user by the given credentials.
*/ */
public function retrieveByCredentials(array $credentials): ?Authenticatable public function retrieveByCredentials(array $credentials): ?User
{ {
return User::query() return User::query()
->where('external_auth_id', $credentials['external_auth_id']) ->where('external_auth_id', $credentials['external_auth_id'])

View File

@@ -50,8 +50,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
/** /**
* Create a new authentication guard. * Create a new authentication guard.
*/ */
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService) public function __construct(
{ string $name,
UserProvider $provider,
Session $session,
RegistrationService $registrationService
) {
$this->name = $name; $this->name = $name;
$this->session = $session; $this->session = $session;
$this->provider = $provider; $this->provider = $provider;

View File

@@ -83,6 +83,10 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
} }
} }
if (!($user instanceof User)) {
throw new LoginAttemptException('Could not find or create a user for LDAP login.');
}
// Sync LDAP groups if required // Sync LDAP groups if required
if ($this->ldapService->shouldSyncGroups()) { if ($this->ldapService->shouldSyncGroups()) {
$this->ldapService->syncGroups($user, $username); $this->ldapService->syncGroups($user, $username);

View File

@@ -131,8 +131,6 @@ class OidcJwtWithClaims implements ProvidesClaims
} }
}, $this->keys); }, $this->keys);
$parsedKeys = array_filter($parsedKeys);
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1]; $contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
/** @var OidcJwtSigningKey $parsedKey */ /** @var OidcJwtSigningKey $parsedKey */
foreach ($parsedKeys as $parsedKey) { foreach ($parsedKeys as $parsedKey) {

View File

@@ -80,6 +80,7 @@ class OidcService
$provider->setPkceCode($pkceCode); $provider->setPkceCode($pkceCode);
// Try to exchange authorization code for access token // Try to exchange authorization code for access token
/** @var OidcAccessToken $accessToken */
$accessToken = $provider->getAccessToken('authorization_code', [ $accessToken = $provider->getAccessToken('authorization_code', [
'code' => $authorizationCode, 'code' => $authorizationCode,
]); ]);

View File

@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
@@ -27,6 +28,7 @@ class ActivityQueries
*/ */
public function latest(int $count = 20, int $page = 0): array public function latest(int $count = 20, int $page = 0): array
{ {
/** @var Collection<int, Activity> $activityList */
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
@@ -83,6 +85,7 @@ class ActivityQueries
*/ */
public function userActivity(User $user, int $count = 20, int $page = 0): array public function userActivity(User $user, int $count = 20, int $page = 0): array
{ {
/** @var Collection<int, Activity> $activityList */
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')

View File

@@ -3,6 +3,7 @@
namespace BookStack\Activity\Models; namespace BookStack\Activity\Models;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\HasCreatorAndUpdater;
@@ -40,6 +41,9 @@ class Comment extends Model implements Loggable, OwnableInterface
/** /**
* Get the entity that this comment belongs to. * Get the entity that this comment belongs to.
* It's only pages right now hence the typing below.
* Would need a deeper audit if that changes as many areas assume this is always a page.
* @return MorphTo<Page, $this>
*/ */
public function entity(): MorphTo public function entity(): MorphTo
{ {
@@ -55,7 +59,9 @@ class Comment extends Model implements Loggable, OwnableInterface
// Ultimately, we could just align the method name to 'commentable' but that would be a potential // Ultimately, we could just align the method name to 'commentable' but that would be a potential
// breaking change and not really worthwhile in a patch due to the risk of creating extra problems. // breaking change and not really worthwhile in a patch due to the risk of creating extra problems.
return $this->morphTo(null, 'commentable_type', 'commentable_id'); /** @var MorphTo<Page, $this> $relation */
$relation = $this->morphTo(null, 'commentable_type', 'commentable_id');
return $relation;
} }
/** /**

View File

@@ -73,13 +73,17 @@ class WebhookFormatter
// Load entity owner, creator, updater details // Load entity owner, creator, updater details
$this->addModelFormatter( $this->addModelFormatter(
fn ($event, $model) => ($model instanceof Entity), fn ($event, $model) => ($model instanceof Entity),
fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy']) function ($model) {
$model->load(['ownedBy', 'createdBy', 'updatedBy']);
}
); );
// Load revision detail for page update and create events // Load revision detail for page update and create events
$this->addModelFormatter( $this->addModelFormatter(
fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)), fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),
fn ($model) => $model->load('currentRevision') function ($model) {
$model->load('currentRevision');
}
); );
} }

View File

@@ -138,8 +138,8 @@ class ApiDocsGenerator
return $validation; return $validation;
} }
if (is_object($validation) && method_exists($validation, '__toString')) { if (is_object($validation) && $validation instanceof \Stringable) {
return strval($validation); return $validation->__toString();
} }
if ($validation instanceof Password) { if ($validation instanceof Password) {

View File

@@ -7,6 +7,7 @@ use BookStack\Theming\ThemeService;
use BookStack\Theming\ThemeViews; use BookStack\Theming\ThemeViews;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\View\FileViewFinder;
class ThemeServiceProvider extends ServiceProvider class ThemeServiceProvider extends ServiceProvider
{ {
@@ -27,7 +28,11 @@ class ThemeServiceProvider extends ServiceProvider
// Boot up the theme system // Boot up the theme system
$themeService = $this->app->make(ThemeService::class); $themeService = $this->app->make(ThemeService::class);
$viewFactory = $this->app->make('view'); $viewFactory = $this->app->make('view');
$themeViews = new ThemeViews($viewFactory->getFinder()); $viewFinder = $viewFactory->getFinder();
if (!($viewFinder instanceof FileViewFinder)) {
throw new \Exception('Only the file view finder is supported for the theme system');
}
$themeViews = new ThemeViews($viewFinder);
// Use a custom include so that we can insert theme views before/after includes. // Use a custom include so that we can insert theme views before/after includes.
// This is done, even if no theme is active, so that view caching does not create problems // This is done, even if no theme is active, so that view caching does not create problems

View File

@@ -6,6 +6,7 @@ use BookStack\Facades\Theme;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
/** /**
@@ -40,7 +41,7 @@ function user(): User
* Check if the current user has a permission. If an ownable element * Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item. * is passed in the jointPermissions are checked against that particular item.
*/ */
function userCan(string|Permission $permission, ?Model $ownable = null): bool function userCan(string|Permission $permission, (Model&OwnableInterface)|null $ownable = null): bool
{ {
if (is_null($ownable)) { if (is_null($ownable)) {
return user()->can($permission); return user()->can($permission);

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Activity\Tools\CommentTree; use BookStack\Activity\Tools\CommentTree;
@@ -10,7 +12,9 @@ use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use Exception; use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
class PageApiController extends ApiController class PageApiController extends ApiController
{ {
@@ -45,7 +49,7 @@ class PageApiController extends ApiController
/** /**
* Get a listing of pages visible to the user. * Get a listing of pages visible to the user.
*/ */
public function list() public function list(): JsonResponse
{ {
$pages = $this->queries->visibleForList() $pages = $this->queries->visibleForList()
->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']); ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
@@ -69,7 +73,7 @@ class PageApiController extends ApiController
* Any images included via base64 data URIs will be extracted and saved as gallery * Any images included via base64 data URIs will be extracted and saved as gallery
* images against the page during upload. * images against the page during upload.
*/ */
public function create(Request $request) public function create(Request $request): JsonResponse
{ {
$this->validate($request, $this->rules['create']); $this->validate($request, $this->rules['create']);
@@ -102,9 +106,9 @@ class PageApiController extends ApiController
* Comments for the page are provided in a tree-structure representing the hierarchy of top-level * Comments for the page are provided in a tree-structure representing the hierarchy of top-level
* comments and replies, for both archived and active comments. * comments and replies, for both archived and active comments.
*/ */
public function read(string $id) public function read(string $id): JsonResponse
{ {
$page = $this->queries->findVisibleByIdOrFail($id); $page = $this->queries->findVisibleByIdOrFail(intval($id));
$page = $page->forJsonDisplay(); $page = $page->forJsonDisplay();
$commentTree = (new CommentTree($page)); $commentTree = (new CommentTree($page));
@@ -124,11 +128,11 @@ class PageApiController extends ApiController
* Providing a 'book_id' or 'chapter_id' property will essentially move * Providing a 'book_id' or 'chapter_id' property will essentially move
* the page into that parent element if you have permissions to do so. * the page into that parent element if you have permissions to do so.
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id): JsonResponse
{ {
$requestData = $this->validate($request, $this->rules['update']); $requestData = $this->validate($request, $this->rules['update']);
$page = $this->queries->findVisibleByIdOrFail($id); $page = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::PageUpdate, $page); $this->checkOwnablePermission(Permission::PageUpdate, $page);
$parent = null; $parent = null;
@@ -161,9 +165,9 @@ class PageApiController extends ApiController
* Delete a page. * Delete a page.
* This will typically send the page to the recycle bin. * This will typically send the page to the recycle bin.
*/ */
public function delete(string $id) public function delete(string $id): Response
{ {
$page = $this->queries->findVisibleByIdOrFail($id); $page = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::PageDelete, $page); $this->checkOwnablePermission(Permission::PageDelete, $page);
$this->pageRepo->destroy($page); $this->pageRepo->destroy($page);

View File

@@ -25,6 +25,7 @@ use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig; use BookStack\Util\HtmlContentFilterConfig;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
@@ -358,8 +359,8 @@ class PageController extends Controller
*/ */
public function showRecentlyUpdated() public function showRecentlyUpdated()
{ {
$visibleBelongsScope = function (BelongsTo $query) { $visibleBelongsScope = function (Relation $relation): void {
$query->scopes('visible'); $relation->scopes('visible');
}; };
$pages = $this->queries->visibleForList() $pages = $this->queries->visibleForList()

View File

@@ -177,7 +177,7 @@ class PageRevisionController extends Controller
*/ */
public function destroyUserDraft(string $pageId) public function destroyUserDraft(string $pageId)
{ {
$page = $this->pageQueries->findVisibleByIdOrFail($pageId); $page = $this->pageQueries->findVisibleByIdOrFail(intval($pageId));
$this->revisionRepo->deleteDraftsForCurrentUser($page); $this->revisionRepo->deleteDraftsForCurrentUser($page);
return response('', 200); return response('', 200);

View File

@@ -20,10 +20,10 @@ use Illuminate\Support\Collection;
* @property ?int $image_id * @property ?int $image_id
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?int $sort_rule_id * @property ?int $sort_rule_id
* @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection<int, Chapter> $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages * @property \Illuminate\Database\Eloquent\Collection<int, Page> $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages * @property \Illuminate\Database\Eloquent\Collection<int, Page> $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves * @property \Illuminate\Database\Eloquent\Collection<int, Bookshelf> $shelves
* @property ?SortRule $sortRule * @property ?SortRule $sortRule
*/ */
class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface

View File

@@ -46,7 +46,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $created_by * @property int|null $created_by
* @property int|null $updated_by * @property int|null $updated_by
* @property int|null $owned_by * @property int|null $owned_by
* @property Collection $tags * @property Collection<int, Tag> $tags
* *
* @method static Entity|Builder visible() * @method static Entity|Builder visible()
* @method static Builder withLastView() * @method static Builder withLastView()

View File

@@ -24,8 +24,8 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property int $revision_count * @property int $revision_count
* @property string $editor * @property string $editor
* @property Chapter|null $chapter * @property Chapter|null $chapter
* @property Collection $attachments * @property Collection<int, Attachment> $attachments
* @property Collection $revisions * @property Collection<int, PageRevision> $revisions
* @property PageRevision $currentRevision * @property PageRevision $currentRevision
*/ */
class Page extends BookChild class Page extends BookChild

View File

@@ -6,6 +6,7 @@ use BookStack\Activity\Models\View;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -20,6 +21,7 @@ class QueryPopular
public function run(int $count, int $page, array $filterModels): Collection public function run(int $count, int $page, array $filterModels): Collection
{ {
/** @var Builder<View> $query */
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type') ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count')) ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))

View File

@@ -5,6 +5,7 @@ namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class QueryRecentlyViewed class QueryRecentlyViewed
@@ -22,6 +23,7 @@ class QueryRecentlyViewed
return collect(); return collect();
} }
/** @var Builder<View> $query */
$query = $this->permissions->restrictEntityRelationQuery( $query = $this->permissions->restrictEntityRelationQuery(
View::query(), View::query(),
'views', 'views',

View File

@@ -5,6 +5,7 @@ namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\Favourite; use BookStack\Activity\Models\Favourite;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
class QueryTopFavourites class QueryTopFavourites
@@ -22,6 +23,7 @@ class QueryTopFavourites
return collect(); return collect();
} }
/** @var Builder<Favourite> $query */
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type') ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
->select('favourites.*') ->select('favourites.*')

View File

@@ -25,9 +25,13 @@ class CustomListItemRenderer implements NodeRendererInterface
*/ */
public function render(Node $node, ChildNodeRendererInterface $childRenderer) public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{ {
if (!($node instanceof ListItem)) {
return null;
}
$listItem = $this->baseRenderer->render($node, $childRenderer); $listItem = $this->baseRenderer->render($node, $childRenderer);
if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) { if ($this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
$listItem->setAttribute('class', 'task-list-item'); $listItem->setAttribute('class', 'task-list-item');
} }

View File

@@ -421,28 +421,41 @@ class PageContent
*/ */
protected function headerNodesToLevelList(DOMNodeList $nodeList): array protected function headerNodesToLevelList(DOMNodeList $nodeList): array
{ {
$tree = collect($nodeList)->map(function (DOMElement $header) { $minLevel = 6;
$headerDetails = array_map(function (DOMNode $header) use (&$minLevel) {
if (!$header instanceof DOMElement) {
return null;
}
$text = trim(str_replace("\xc2\xa0", ' ', $header->nodeValue)); $text = trim(str_replace("\xc2\xa0", ' ', $header->nodeValue));
$text = mb_substr($text, 0, 100); $text = mb_substr($text, 0, 100);
if (empty($text)) {
return null;
}
$level = intval(str_replace('h', '', $header->nodeName));
if ($level < $minLevel) {
$minLevel = $level;
}
return [ return [
'nodeName' => strtolower($header->nodeName), 'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)), 'level' => $level,
'link' => '#' . $header->getAttribute('id'), 'link' => '#' . $header->getAttribute('id'),
'text' => $text, 'text' => $text,
]; ];
})->filter(function ($header) { }, [...$nodeList]);
return mb_strlen($header['text']) > 0;
}); $filtered = array_values(array_filter($headerDetails));
// Shift headers if only smaller headers have been used // Shift headers if only smaller headers have been used
$levelChange = ($tree->pluck('level')->min() - 1); $levelChange = ($minLevel - 1);
$tree = $tree->map(function ($header) use ($levelChange) { foreach ($filtered as $index => $header) {
$header['level'] -= ($levelChange); $filtered[$index]['level'] -= $levelChange;
}
return $header; return $filtered;
});
return $tree->toArray();
} }
} }

View File

@@ -192,10 +192,10 @@ class PageIncludeParser
/** /**
* Get the parent paragraph of the given node, if existing. * Get the parent paragraph of the given node, if existing.
*/ */
protected function getParentParagraph(DOMNode $parent): ?DOMNode protected function getParentParagraph(DOMNode $parent): ?DOMElement
{ {
do { do {
if (strtolower($parent->nodeName) === 'p') { if (strtolower($parent->nodeName) === 'p' && $parent instanceof DOMElement) {
return $parent; return $parent;
} }

View File

@@ -52,7 +52,7 @@ class SlugGenerator
{ {
$slug = Str::slug($name); $slug = Str::slug($name);
if ($slug === '') { if ($slug === '') {
$slug = substr(md5(rand(1, 500)), 0, 5); $slug = substr(md5(strval(rand(1, 500))), 0, 5);
} }
return $slug; return $slug;

View File

@@ -3,6 +3,8 @@
namespace BookStack\Exports; namespace BookStack\Exports;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
@@ -119,6 +121,9 @@ class ImportRepo
$parentModel = null; $parentModel = null;
if ($import->type === 'page' || $import->type === 'chapter') { if ($import->type === 'page' || $import->type === 'chapter') {
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null; $parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
if ($parentModel && !($parentModel instanceof Book || $parentModel instanceof Chapter)) {
throw new ZipImportException(['Selected parent is not a book or chapter']);
}
} }
DB::beginTransaction(); DB::beginTransaction();

View File

@@ -48,7 +48,7 @@ class ZipImportRunner
* Returns the top-level entity item which was imported. * Returns the top-level entity item which was imported.
* @throws ZipImportException * @throws ZipImportException
*/ */
public function run(Import $import, ?Entity $parent = null): Entity public function run(Import $import, Book|Chapter|null $parent = null): Entity
{ {
$zipPath = $this->getZipPath($import); $zipPath = $this->getZipPath($import);
$reader = new ZipExportReader($zipPath); $reader = new ZipExportReader($zipPath);

View File

@@ -7,6 +7,7 @@ use BookStack\App\Model;
use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Users\Models\OwnableInterface;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -80,7 +81,7 @@ abstract class Controller extends BaseController
/** /**
* Check the current user's permissions against an ownable item otherwise throw an exception. * Check the current user's permissions against an ownable item otherwise throw an exception.
*/ */
protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void protected function checkOwnablePermission(string|Permission $permission, Model&OwnableInterface $ownable, string $redirectLocation = '/'): void
{ {
if (!userCan($permission, $ownable)) { if (!userCan($permission, $ownable)) {
$this->showPermissionError($redirectLocation); $this->showPermissionError($redirectLocation);

View File

@@ -101,6 +101,7 @@ class JointPermissionBuilder
/** /**
* Get a query for fetching a book with its children. * Get a query for fetching a book with its children.
* @return Builder<Book>
*/ */
protected function bookFetchQuery(): Builder protected function bookFetchQuery(): Builder
{ {
@@ -117,9 +118,11 @@ class JointPermissionBuilder
/** /**
* Build joint permissions for the given book and role combinations. * Build joint permissions for the given book and role combinations.
* @param EloquentCollection<int, Book> $books
*/ */
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void
{ {
/** @var EloquentCollection<int, Entity> $entities */
$entities = clone $books; $entities = clone $books;
/** @var Book $book */ /** @var Book $book */

View File

@@ -2,8 +2,8 @@
namespace BookStack\References; namespace BookStack\References;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;

View File

@@ -22,6 +22,7 @@ class ReferenceFetcher
*/ */
public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection
{ {
/** @var Collection<int, Reference> $references */
$references = $this->queryReferencesToEntity($entity)->get(); $references = $this->queryReferencesToEntity($entity)->get();
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents); $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents);
@@ -37,6 +38,9 @@ class ReferenceFetcher
return $this->queryReferencesToEntity($entity)->count(); return $this->queryReferencesToEntity($entity)->count();
} }
/**
* @return Builder<Reference>
*/
protected function queryReferencesToEntity(Entity $entity): Builder protected function queryReferencesToEntity(Entity $entity): Builder
{ {
$baseQuery = Reference::query() $baseQuery = Reference::query()

View File

@@ -4,6 +4,7 @@ namespace BookStack\Search;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\EntityTable;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\EntityHydrator; use BookStack\Entities\Tools\EntityHydrator;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
@@ -90,6 +91,8 @@ class SearchRunner
/** /**
* Get a page of result data from the given query based on the provided page parameters. * Get a page of result data from the given query based on the provided page parameters.
* @param EloquentBuilder<EntityTable> $query
* @return Collection<Entity>
*/ */
protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection
{ {
@@ -106,6 +109,7 @@ class SearchRunner
/** /**
* Create a search query for an entity. * Create a search query for an entity.
* @param string[] $entityTypes * @param string[] $entityTypes
* @return EloquentBuilder<EntityTable>
*/ */
protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder
{ {

View File

@@ -243,9 +243,9 @@ class SettingService
/** /**
* Convert a setting key into a user-specific key. * Convert a setting key into a user-specific key.
*/ */
protected function userKey(string $userId, string $key = ''): string protected function userKey(int $userId, string $key = ''): string
{ {
return 'user:' . $userId . ':' . $key; return 'user:' . strval($userId) . ':' . $key;
} }
/** /**
@@ -267,7 +267,7 @@ class SettingService
/** /**
* Delete settings for a given user id. * Delete settings for a given user id.
*/ */
public function deleteUserSettings(string $userId): void public function deleteUserSettings(int $userId): void
{ {
Setting::query() Setting::query()
->where('setting_key', 'like', $this->userKey($userId) . '%') ->where('setting_key', 'like', $this->userKey($userId) . '%')

View File

@@ -38,21 +38,35 @@ class SortRule extends Model implements Loggable
$this->sequence = implode(',', $values); $this->sequence = implode(',', $values);
} }
/**
* @inheritDoc
*/
public function logDescriptor(): string public function logDescriptor(): string
{ {
return "({$this->id}) {$this->name}"; return "({$this->id}) {$this->name}";
} }
/**
* Get the URL to where this rule can be managed.
*/
public function getUrl(): string public function getUrl(): string
{ {
return url("/settings/sorting/rules/{$this->id}"); return url("/settings/sorting/rules/{$this->id}");
} }
/**
* Get the books which are specifically set to use this sort rule.
* @return HasMany<Book, $this>
*/
public function books(): HasMany public function books(): HasMany
{ {
return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id'); return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id');
} }
/**
* Get all the available sort rules, ordered by name, with the number of books using each.
* @return Collection<SortRule>
*/
public static function allByName(): Collection public static function allByName(): Collection
{ {
return static::query() return static::query()

View File

@@ -102,7 +102,7 @@ class AttachmentService
/** /**
* Updates the ordering for a listing of attached files. * Updates the ordering for a listing of attached files.
*/ */
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId) public function updateFileOrderWithinPage(array $attachmentOrder, int $pageId)
{ {
foreach ($attachmentOrder as $index => $attachmentId) { foreach ($attachmentOrder as $index => $attachmentId) {
Attachment::query()->where('uploaded_to', '=', $pageId) Attachment::query()->where('uploaded_to', '=', $pageId)

View File

@@ -87,7 +87,7 @@ class RoleApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById(intval($id));
$this->singleFormatter($role); $this->singleFormatter($role);
return response()->json($role); return response()->json($role);

View File

@@ -94,7 +94,7 @@ class RoleController extends Controller
public function edit(string $id) public function edit(string $id)
{ {
$this->checkPermission(Permission::UserRolesManage); $this->checkPermission(Permission::UserRolesManage);
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById(intval($id));
$this->setPageTitle(trans('settings.role_edit')); $this->setPageTitle(trans('settings.role_edit'));
@@ -129,7 +129,7 @@ class RoleController extends Controller
public function showDelete(string $id) public function showDelete(string $id)
{ {
$this->checkPermission(Permission::UserRolesManage); $this->checkPermission(Permission::UserRolesManage);
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById(intval($id));
$roles = $this->permissionsRepo->getAllRolesExcept($role); $roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole); $roles->prepend($blankRole);
@@ -151,7 +151,7 @@ class RoleController extends Controller
try { try {
$migrateRoleId = intval($request->input('migrate_role_id') ?: "0"); $migrateRoleId = intval($request->input('migrate_role_id') ?: "0");
$this->permissionsRepo->deleteRole($id, $migrateRoleId); $this->permissionsRepo->deleteRole(intval($id), $migrateRoleId);
} catch (PermissionsException $e) { } catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage()); $this->showErrorNotification($e->getMessage());

View File

@@ -110,7 +110,7 @@ class UserApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById(intval($id));
$this->singleFormatter($user); $this->singleFormatter($user);
return response()->json($user); return response()->json($user);
@@ -124,8 +124,8 @@ class UserApiController extends ApiController
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id)
{ {
$data = $this->validate($request, $this->rules($id)['update']); $data = $this->validate($request, $this->rules(intval($id))['update']);
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById(intval($id));
$this->userRepo->update($user, $data, userCan(Permission::UsersManage)); $this->userRepo->update($user, $data, userCan(Permission::UsersManage));
$this->singleFormatter($user); $this->singleFormatter($user);
@@ -140,7 +140,7 @@ class UserApiController extends ApiController
*/ */
public function delete(Request $request, string $id) public function delete(Request $request, string $id)
{ {
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById(intval($id));
$newOwnerId = $request->input('migrate_ownership_id', null); $newOwnerId = $request->input('migrate_ownership_id', null);
$this->userRepo->destroy($user, $newOwnerId); $this->userRepo->destroy($user, $newOwnerId);

View File

@@ -3,6 +3,7 @@
namespace BookStack\Util; namespace BookStack\Util;
use Closure; use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Throwable; use Throwable;
@@ -24,7 +25,7 @@ use Throwable;
class DatabaseTransaction class DatabaseTransaction
{ {
/** /**
* @param (Closure(static): TReturn) $callback * @param (Closure(Connection): TReturn) $callback
*/ */
public function __construct( public function __construct(
protected Closure $callback protected Closure $callback

View File

@@ -7,7 +7,7 @@ parameters:
- app - app
# The level 8 is the highest level # The level 8 is the highest level
level: 4 level: 5
phpVersion: phpVersion:
min: 80200 min: 80200