$queryIds = [$entity->getMorphClass() => [$entity->id]];
if ($entity instanceof Book) {
- $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
+ $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->scopes('visible')->pluck('id');
}
if ($entity instanceof Book || $entity instanceof Chapter) {
- $queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id');
+ $queryIds[(new Page())->getMorphClass()] = $entity->pages()->scopes('visible')->pluck('id');
}
$query = $this->activity->newQuery();
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
*
- * @param $connection
+ * @param resource $connection
*
* @throws LdapException
*/
protected function loadFromPath(string $path)
{
try {
- $this->key = PublicKeyLoader::load(
+ $key = PublicKeyLoader::load(
file_get_contents($path)
- )->withPadding(RSA::SIGNATURE_PKCS1);
+ );
} catch (\Exception $exception) {
throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
}
- if (!($this->key instanceof RSA)) {
+ if (!$key instanceof RSA) {
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
}
+
+ $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
}
/**
$n = strtr($jwk['n'] ?? '', '-_', '+/');
try {
- /** @var RSA $key */
- $this->key = PublicKeyLoader::load([
+ $key = PublicKeyLoader::load([
'e' => new BigInteger(base64_decode($jwk['e']), 256),
'n' => new BigInteger(base64_decode($n), 256),
- ])->withPadding(RSA::SIGNATURE_PKCS1);
+ ]);
} catch (\Exception $exception) {
throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
}
+
+ if (!$key instanceof RSA) {
+ throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
+ }
+
+ $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
}
/**
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Contracts\User as SocialUser;
+use Laravel\Socialite\Two\GoogleProvider;
use SocialiteProviders\Manager\SocialiteWasCalled;
use Symfony\Component\HttpFoundation\RedirectResponse;
{
$driver = $this->socialite->driver($driverName);
- if ($driverName === 'google' && config('services.google.select_account')) {
+ if ($driver instanceof GoogleProvider && config('services.google.select_account')) {
$driver->with(['prompt' => 'select_account']);
}
/**
* Get all pages within this book.
- *
- * @return HasMany
*/
- public function pages()
+ public function pages(): HasMany
{
return $this->hasMany(Page::class);
}
/**
* Get the direct child pages of this book.
- *
- * @return HasMany
*/
- public function directPages()
+ public function directPages(): HasMany
{
return $this->pages()->where('chapter_id', '=', '0');
}
/**
* Get all chapters within this book.
- *
- * @return HasMany
*/
- public function chapters()
+ public function chapters(): HasMany
{
return $this->hasMany(Chapter::class);
}
/**
* Get the shelves this book is contained within.
- *
- * @return BelongsToMany
*/
- public function shelves()
+ public function shelves(): BelongsToMany
{
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
}
/**
* Get the direct child items within this book.
- *
- * @return Collection
*/
public function getDirectChildren(): Collection
{
- $pages = $this->directPages()->visible()->get();
- $chapters = $this->chapters()->visible()->get();
+ $pages = $this->directPages()->scopes('visible')->get();
+ $chapters = $this->chapters()->scopes('visible')->get();
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
*/
public function visibleBooks(): BelongsToMany
{
- return $this->books()->visible();
+ return $this->books()->scopes('visible');
}
/**
/**
* Get the pages that this chapter contains.
+ * @return HasMany<Page>
*/
public function pages(string $dir = 'ASC'): HasMany
{
*/
public function getVisiblePages(): Collection
{
- return $this->pages()->visible()
+ return $this->pages()
+ ->scopes('visible')
->orderBy('draft', 'desc')
->orderBy('priority', 'asc')
->get();
return true;
}
- if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
+ if (($entity instanceof BookChild) && $this instanceof Book) {
return $entity->book_id === $this->id;
}
- if ($entity->isA('page') && $this->isA('chapter')) {
+ if ($entity instanceof Page && $this instanceof Chapter) {
return $entity->chapter_id === $this->id;
}
/**
* Get the previous revision for the same page if existing.
- *
- * @return \BookStack\Entities\PageRevision|null
*/
- public function getPrevious()
+ public function getPrevious(): ?PageRevision
{
$id = static::newQuery()->where('page_id', '=', $this->page_id)
->where('id', '<', $this->id)
*/
public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
{
+ /** @var ?PageRevision $revision */
$revision = PageRevision::query()
->whereHas('page', function (Builder $query) {
- $query->visible();
+ $query->scopes('visible');
})
->where('slug', '=', $pageSlug)
->where('type', '=', 'version')
->with('page')
->first();
- return $revision ? $revision->page : null;
+ return $revision->page ?? null;
}
/**
use BookStack\Uploads\ImageService;
use BookStack\Util\HtmlContentFilter;
use DOMDocument;
+use DOMElement;
+use DOMNode;
use DOMNodeList;
use DOMXPath;
use Illuminate\Support\Str;
* A map for existing ID's should be passed in to check for current existence.
* Returns a pair of strings in the format [old_id, new_id].
*/
- protected function setUniqueId(\DOMNode $element, array &$idMap): array
+ protected function setUniqueId(DOMNode $element, array &$idMap): array
{
- if (!$element instanceof \DOMElement) {
+ if (!$element instanceof DOMElement) {
return ['', ''];
}
*/
protected function headerNodesToLevelList(DOMNodeList $nodeList): array
{
- $tree = collect($nodeList)->map(function ($header) {
+ $tree = collect($nodeList)->map(function (DOMElement $header) {
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
$text = mb_substr($text, 0, 100);
use BookStack\Entities\Models\SearchTerm;
use DOMDocument;
use DOMNode;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class SearchIndex
foreach ($this->entityProvider->all() as $entityModel) {
$indexContentField = $entityModel instanceof Page ? 'html' : 'description';
$selectFields = ['id', 'name', $indexContentField];
- $total = $entityModel->newQuery()->withTrashed()->count();
+ /** @var Builder<Entity> $query */
+ $query = $entityModel->newQuery();
+ $total = $query->withTrashed()->count();
$chunkSize = 250;
$processed = 0;
if ($entity instanceof Page) {
$bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html);
} else {
- $bodyTermsMap = $this->generateTermScoreMapFromText($entity->description ?? '', $entity->searchFactor);
+ $bodyTermsMap = $this->generateTermScoreMapFromText($entity->getAttribute('description') ?? '', $entity->searchFactor);
}
$mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap, $tagTermsMap);
if ($entityModelInstance instanceof BookChild) {
$relations['book'] = function (BelongsTo $query) {
- $query->visible();
+ $query->scopes('visible');
};
}
if ($entityModelInstance instanceof Page) {
$relations['chapter'] = function (BelongsTo $query) {
- $query->visible();
+ $query->scopes('visible');
};
}
use BookStack\Uploads\AttachmentService;
use BookStack\Uploads\ImageService;
use Exception;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
class TrashCan
{
$count = 0;
$pages = $chapter->pages()->withTrashed()->get();
- if (count($pages)) {
- foreach ($pages as $page) {
- $this->destroyPage($page);
- $count++;
- }
+ foreach ($pages as $page) {
+ $this->destroyPage($page);
+ $count++;
}
$this->destroyCommonRelations($chapter);
{
$counts = [];
- /** @var Entity $instance */
foreach ((new EntityProvider())->all() as $key => $instance) {
- $counts[$key] = $instance->newQuery()->onlyTrashed()->count();
+ /** @var Builder<Entity> $query */
+ $query = $instance->newQuery();
+ $counts[$key] = $query->onlyTrashed()->count();
}
return $counts;
$entity->deletions()->delete();
$entity->favourites()->delete();
- if ($entity instanceof HasCoverImage && $entity->cover) {
+ if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
$imageService = app()->make(ImageService::class);
- $imageService->destroy($entity->cover);
+ $imageService->destroy($entity->cover()->first());
}
}
}
$shelf = Bookshelf::visible()->with([
'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
'books' => function (BelongsToMany $query) {
- $query->visible()->get(['id', 'name', 'slug']);
+ $query->scopes('visible')->get(['id', 'name', 'slug']);
},
])->findOrFail($id);
public function read(string $id)
{
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
- $query->visible()->get(['id', 'name', 'slug']);
+ $query->scopes('visible')->get(['id', 'name', 'slug']);
}])->findOrFail($id);
return response()->json($chapter);
{
$book = $this->bookRepo->getBySlug($slug);
$bookChildren = (new BookContents($book))->getTree(true);
- $bookParentShelves = $book->shelves()->visible()->get();
+ $bookParentShelves = $book->shelves()->scopes('visible')->get();
View::incrementFor($book);
if ($request->has('shelf')) {
*/
public function registerCommand(Command $command)
{
- Artisan::starting(function(Application $application) use ($command) {
+ Artisan::starting(function (Application $application) use ($command) {
$application->addCommands([$command]);
});
}
if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') {
- $validPageIds = $contextPage->book->pages()->visible()->pluck('id')->toArray();
+ $validPageIds = $contextPage->book->pages()
+ ->scopes('visible')
+ ->pluck('id')
+ ->toArray();
$query->whereIn('uploaded_to', $validPageIds);
}
};
--- /dev/null
+<?php
+
+namespace Tests\Unit;
+
+use BadMethodCallException;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+/**
+ * This class tests assumptions we're relying upon in the framework.
+ * This is primarily to keep track of certain bits of functionality that
+ * may be used in important areas such as to enforce permissions.
+ */
+class FrameworkAssumptionTest extends TestCase
+{
+
+ public function test_scopes_error_if_not_existing()
+ {
+ $this->expectException(BadMethodCallException::class);
+ $this->expectExceptionMessage('Call to undefined method BookStack\Entities\Models\Page::scopeNotfoundscope()');
+ Page::query()->scopes('notfoundscope');
+ }
+
+ public function test_scopes_applies_upon_existing()
+ {
+ // Page has SoftDeletes trait by default, so we apply our custom scope and ensure
+ // it stacks on the global scope to filter out deleted items.
+ $query = Page::query()->scopes('visible')->toSql();
+ $this->assertStringContainsString('joint_permissions', $query);
+ $this->assertStringContainsString('`deleted_at` is null', $query);
+ }
+}