From 754403a29e90f05a64ceca581580ef341ff910a8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 18 Nov 2021 21:04:25 +0000 Subject: [PATCH 01/14] Added video guide link to visual theme system docs --- dev/docs/visual-theme-system.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/docs/visual-theme-system.md b/dev/docs/visual-theme-system.md index 058bd2823..8f3129b22 100644 --- a/dev/docs/visual-theme-system.md +++ b/dev/docs/visual-theme-system.md @@ -6,6 +6,8 @@ This theme system itself is maintained and supported but usages of this system, ## Getting Started +*[Video Guide](https://www.youtube.com/watch?v=gLy_2GBse48)* + This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder. You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`. @@ -28,4 +30,4 @@ As an example, Say I wanted to change 'Search' to 'Find'; Within a `themes/ 'find', ]; -``` \ No newline at end of file +``` From 876bc10d4d414c0680ea13e008b764c3ee70060f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 20 Nov 2021 14:03:56 +0000 Subject: [PATCH 02/14] Applied another set of static analysis improvements --- app/Actions/ActivityService.php | 7 ++++--- app/Auth/Access/ExternalBaseUserProvider.php | 15 +++++++-------- app/Auth/Permissions/RolePermission.php | 9 +++------ app/Console/Commands/RegenerateSearch.php | 2 +- app/Entities/Models/Deletion.php | 3 ++- app/Entities/Models/Entity.php | 4 +++- app/Entities/Models/PageRevision.php | 6 ++---- app/Entities/Repos/BaseRepo.php | 4 +++- app/Entities/Repos/PageRepo.php | 8 ++++++-- app/Entities/Tools/BookContents.php | 4 ++-- app/Entities/Tools/NextPreviousContentLocator.php | 2 +- app/Entities/Tools/PageContent.php | 6 +++--- app/Entities/Tools/SearchIndex.php | 2 +- app/Entities/Tools/SearchRunner.php | 5 ++++- app/Entities/Tools/SiblingFetcher.php | 3 ++- app/Entities/Tools/TrashCan.php | 10 ++++++---- app/Http/Controllers/RecycleBinController.php | 1 + app/Interfaces/Deletable.php | 14 ++++++++++++++ 18 files changed, 65 insertions(+), 40 deletions(-) create mode 100644 app/Interfaces/Deletable.php diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php index bc7a6b6b7..33ed44b32 100644 --- a/app/Actions/ActivityService.php +++ b/app/Actions/ActivityService.php @@ -4,6 +4,7 @@ namespace BookStack\Actions; use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\User; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; @@ -100,13 +101,13 @@ class ActivityService */ public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array { - /** @var [string => int[]] $queryIds */ + /** @var array $queryIds */ $queryIds = [$entity->getMorphClass() => [$entity->id]]; - if ($entity->isA('book')) { + if ($entity instanceof Book) { $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id'); } - if ($entity->isA('book') || $entity->isA('chapter')) { + if ($entity instanceof Book || $entity instanceof Chapter) { $queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id'); } diff --git a/app/Auth/Access/ExternalBaseUserProvider.php b/app/Auth/Access/ExternalBaseUserProvider.php index fde610c3e..dc765ddc5 100644 --- a/app/Auth/Access/ExternalBaseUserProvider.php +++ b/app/Auth/Access/ExternalBaseUserProvider.php @@ -4,6 +4,7 @@ namespace BookStack\Auth\Access; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; +use Illuminate\Database\Eloquent\Model; class ExternalBaseUserProvider implements UserProvider { @@ -16,8 +17,6 @@ class ExternalBaseUserProvider implements UserProvider /** * LdapUserProvider constructor. - * - * @param $model */ public function __construct(string $model) { @@ -27,7 +26,7 @@ class ExternalBaseUserProvider implements UserProvider /** * Create a new instance of the model. * - * @return \Illuminate\Database\Eloquent\Model + * @return Model */ public function createModel() { @@ -41,7 +40,7 @@ class ExternalBaseUserProvider implements UserProvider * * @param mixed $identifier * - * @return \Illuminate\Contracts\Auth\Authenticatable|null + * @return Authenticatable|null */ public function retrieveById($identifier) { @@ -54,7 +53,7 @@ class ExternalBaseUserProvider implements UserProvider * @param mixed $identifier * @param string $token * - * @return \Illuminate\Contracts\Auth\Authenticatable|null + * @return Authenticatable|null */ public function retrieveByToken($identifier, $token) { @@ -64,7 +63,7 @@ class ExternalBaseUserProvider implements UserProvider /** * Update the "remember me" token for the given user in storage. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param Authenticatable $user * @param string $token * * @return void @@ -79,7 +78,7 @@ class ExternalBaseUserProvider implements UserProvider * * @param array $credentials * - * @return \Illuminate\Contracts\Auth\Authenticatable|null + * @return Authenticatable|null */ public function retrieveByCredentials(array $credentials) { @@ -94,7 +93,7 @@ class ExternalBaseUserProvider implements UserProvider /** * Validate a user against the given credentials. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param Authenticatable $user * @param array $credentials * * @return bool diff --git a/app/Auth/Permissions/RolePermission.php b/app/Auth/Permissions/RolePermission.php index 0a0e6ff17..f34de917c 100644 --- a/app/Auth/Permissions/RolePermission.php +++ b/app/Auth/Permissions/RolePermission.php @@ -4,6 +4,7 @@ namespace BookStack\Auth\Permissions; use BookStack\Auth\Role; use BookStack\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * @property int $id @@ -13,19 +14,15 @@ class RolePermission extends Model /** * The roles that belong to the permission. */ - public function roles() + public function roles(): BelongsToMany { return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id'); } /** * Get the permission object by name. - * - * @param $name - * - * @return mixed */ - public static function getByName($name) + public static function getByName(string $name): ?RolePermission { return static::where('name', '=', $name)->first(); } diff --git a/app/Console/Commands/RegenerateSearch.php b/app/Console/Commands/RegenerateSearch.php index 62ee88fc0..20e3fc798 100644 --- a/app/Console/Commands/RegenerateSearch.php +++ b/app/Console/Commands/RegenerateSearch.php @@ -49,7 +49,7 @@ class RegenerateSearch extends Command DB::setDefaultConnection($this->option('database')); } - $this->searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total) { + $this->searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total): void { $this->info('Indexed ' . class_basename($model) . ' entries (' . $processed . '/' . $total . ')'); }); diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php index 3face841b..97abb87ff 100644 --- a/app/Entities/Models/Deletion.php +++ b/app/Entities/Models/Deletion.php @@ -3,13 +3,14 @@ namespace BookStack\Entities\Models; use BookStack\Auth\User; +use BookStack\Interfaces\Deletable; use BookStack\Interfaces\Loggable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; /** - * @property Model $deletable + * @property Deletable $deletable */ class Deletion extends Model implements Loggable { diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 4c4e55bb8..eccec40b5 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -12,6 +12,7 @@ use BookStack\Auth\Permissions\JointPermission; use BookStack\Entities\Tools\SearchIndex; use BookStack\Entities\Tools\SlugGenerator; use BookStack\Facades\Permissions; +use BookStack\Interfaces\Deletable; use BookStack\Interfaces\Favouritable; use BookStack\Interfaces\Sluggable; use BookStack\Interfaces\Viewable; @@ -44,7 +45,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static Builder withLastView() * @method static Builder withViewCount() */ -abstract class Entity extends Model implements Sluggable, Favouritable, Viewable +abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable { use SoftDeletes; use HasCreatorAndUpdater; @@ -210,6 +211,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable /** * Check if this instance or class is a certain type of entity. * Examples of $type are 'page', 'book', 'chapter'. + * @deprecated Use instanceof instead. */ public static function isA(string $type): bool { diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php index 6856c23e1..a3c4379a6 100644 --- a/app/Entities/Models/PageRevision.php +++ b/app/Entities/Models/PageRevision.php @@ -84,11 +84,9 @@ class PageRevision extends Model * Included here to align with entities in similar use cases. * (Yup, Bit of an awkward hack). * - * @param $type - * - * @return bool + * @deprecated Use instanceof instead. */ - public static function isA($type) + public static function isA(string $type): bool { return $type === 'revision'; } diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 569918503..6b29dad7b 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -67,10 +67,12 @@ class BaseRepo /** * Update the given items' cover image, or clear it. * + * @param Entity&HasCoverImage $entity + * * @throws ImageUploadException * @throws \Exception */ - public function updateCoverImage(HasCoverImage $entity, ?UploadedFile $coverImage, bool $removeImage = false) + public function updateCoverImage($entity, ?UploadedFile $coverImage, bool $removeImage = false) { if ($coverImage) { $this->imageRepo->destroyImage($entity->cover); diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index f66f2beb8..ed5534f08 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -290,6 +290,8 @@ class PageRepo public function restoreRevision(Page $page, int $revisionId): Page { $page->revision_count++; + + /** @var PageRevision $revision */ $revision = $page->revisions()->where('id', '=', $revisionId)->first(); $page->fill($revision->toArray()); @@ -334,7 +336,8 @@ class PageRepo } $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null; - $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id); + $newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id; + $page->changeBook($newBookId); $page->rebuildPermissions(); Activity::addForEntity($page, ActivityType::PAGE_MOVE); @@ -406,7 +409,7 @@ class PageRepo */ protected function changeParent(Page $page, Entity $parent) { - $book = ($parent instanceof Book) ? $parent : $parent->book; + $book = ($parent instanceof Chapter) ? $parent->book : $parent; $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0; $page->save(); @@ -467,6 +470,7 @@ class PageRepo { $parent = $page->getParent(); if ($parent instanceof Chapter) { + /** @var ?Page $lastPage */ $lastPage = $parent->pages('desc')->first(); return $lastPage ? $lastPage->priority + 1 : 0; diff --git a/app/Entities/Tools/BookContents.php b/app/Entities/Tools/BookContents.php index 8622d5e12..9b2190ca2 100644 --- a/app/Entities/Tools/BookContents.php +++ b/app/Entities/Tools/BookContents.php @@ -67,7 +67,7 @@ class BookContents $all->each(function (Entity $entity) use ($renderPages) { $entity->setRelation('book', $this->book); - if ($renderPages && $entity->isA('page')) { + if ($renderPages && $entity instanceof Page) { $entity->html = (new PageContent($entity))->render(); } }); @@ -151,7 +151,7 @@ class BookContents $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort); $bookChanged = intval($model->book_id) !== intval($sortMapItem->book); - $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter; + $chapterChanged = ($model instanceof Page) && intval($model->chapter_id) !== $sortMapItem->parentChapter; if ($bookChanged) { $model->changeBook($sortMapItem->book); diff --git a/app/Entities/Tools/NextPreviousContentLocator.php b/app/Entities/Tools/NextPreviousContentLocator.php index f70abd9b6..54b575935 100644 --- a/app/Entities/Tools/NextPreviousContentLocator.php +++ b/app/Entities/Tools/NextPreviousContentLocator.php @@ -64,7 +64,7 @@ class NextPreviousContentLocator /** @var Entity $item */ foreach ($bookTree->all() as $item) { $flatOrdered->push($item); - $childPages = $item->visible_pages ?? []; + $childPages = $item->getAttribute('visible_pages') ?? []; $flatOrdered = $flatOrdered->concat($childPages); } diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 45bfe8fa1..a6ba352a8 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -156,7 +156,7 @@ class PageContent /** * Parse a base64 image URI into the data and extension. * - * @return array{extension: array, data: string} + * @return array{extension: string, data: string} */ protected function parseBase64ImageUri(string $uri): array { @@ -230,7 +230,7 @@ class PageContent */ protected function setUniqueId(\DOMNode $element, array &$idMap): array { - if (get_class($element) !== 'DOMElement') { + if (!$element instanceof \DOMElement) { return ['', '']; } @@ -242,7 +242,7 @@ class PageContent return [$existingId, $existingId]; } - // Create an unique id for the element + // Create a unique id for the element // Uses the content as a basis to ensure output is the same every time // the same content is passed through. $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20); diff --git a/app/Entities/Tools/SearchIndex.php b/app/Entities/Tools/SearchIndex.php index d748c1695..79139ec24 100644 --- a/app/Entities/Tools/SearchIndex.php +++ b/app/Entities/Tools/SearchIndex.php @@ -67,7 +67,7 @@ class SearchIndex * - The number that have been processed so far. * - The total number of that model to be processed. * - * @param callable(Entity, int, int)|null $progressCallback + * @param callable(Entity, int, int):void|null $progressCallback */ public function indexAllEntities(?callable $progressCallback = null) { diff --git a/app/Entities/Tools/SearchRunner.php b/app/Entities/Tools/SearchRunner.php index 04f4c5768..a4f131482 100644 --- a/app/Entities/Tools/SearchRunner.php +++ b/app/Entities/Tools/SearchRunner.php @@ -9,6 +9,7 @@ use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Entities\Models\SearchTerm; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -356,7 +357,9 @@ class SearchRunner // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will // search the value as a string which prevents being able to do number-based operations // on the tag values. We ensure it has a numeric value and then cast it just to be sure. - $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'"); + /** @var Connection $connection */ + $connection = $query->getConnection(); + $tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'"); $query->whereRaw("value ${tagOperator} ${tagValue}"); } else { $query->where('value', $tagOperator, $tagValue); diff --git a/app/Entities/Tools/SiblingFetcher.php b/app/Entities/Tools/SiblingFetcher.php index 249e0038e..617ef4a62 100644 --- a/app/Entities/Tools/SiblingFetcher.php +++ b/app/Entities/Tools/SiblingFetcher.php @@ -5,6 +5,7 @@ namespace BookStack\Entities\Tools; use BookStack\Entities\EntityProvider; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; +use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use Illuminate\Support\Collection; @@ -24,7 +25,7 @@ class SiblingFetcher } // Page in book or chapter - if (($entity instanceof Page && !$entity->chapter) || $entity->isA('chapter')) { + if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) { $entities = $entity->book->getDirectChildren(); } diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 8327c9489..fcf933726 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -235,13 +235,15 @@ class TrashCan { $shouldRestore = true; $restoreCount = 0; - $parent = $deletion->deletable->getParent(); - if ($parent && $parent->trashed()) { - $shouldRestore = false; + if ($deletion->deletable instanceof Entity) { + $parent = $deletion->deletable->getParent(); + if ($parent && $parent->trashed()) { + $shouldRestore = false; + } } - if ($shouldRestore) { + if ($deletion->deletable instanceof Entity && $shouldRestore) { $restoreCount = $this->restoreEntity($deletion->deletable); } diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php index 1736023a5..1cffb161c 100644 --- a/app/Http/Controllers/RecycleBinController.php +++ b/app/Http/Controllers/RecycleBinController.php @@ -58,6 +58,7 @@ class RecycleBinController extends Controller $searching = false; } } + /** @var ?Deletion $parentDeletion */ $parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first(); diff --git a/app/Interfaces/Deletable.php b/app/Interfaces/Deletable.php new file mode 100644 index 000000000..ca59e04f3 --- /dev/null +++ b/app/Interfaces/Deletable.php @@ -0,0 +1,14 @@ + Date: Sun, 21 Nov 2021 15:40:11 -0500 Subject: [PATCH 03/14] Set taborder for TOTP Verification Adding tabindex=0 means when pressing tab the focus goes right to the TOTP input field. When using a Password Manager this makes it easier than having to hit tab 3X to get the right focus. --- resources/views/mfa/parts/verify-totp.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/mfa/parts/verify-totp.blade.php b/resources/views/mfa/parts/verify-totp.blade.php index 9a861fc6c..c155f1bfe 100644 --- a/resources/views/mfa/parts/verify-totp.blade.php +++ b/resources/views/mfa/parts/verify-totp.blade.php @@ -6,6 +6,7 @@ {{ csrf_field() }} @if($errors->has('code')) @@ -14,4 +15,4 @@
- \ No newline at end of file + From f8c16494fd3d8ed8f38742f026eb26fbcafd338b Mon Sep 17 00:00:00 2001 From: julesdevops Date: Thu, 18 Nov 2021 23:01:37 +0100 Subject: [PATCH 04/14] feat(PageContent): set unique ids on nested headers --- app/Entities/Tools/PageContent.php | 9 +++++++++ tests/Entity/PageContentTest.php | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 45bfe8fa1..c8204a181 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -193,6 +193,15 @@ class PageContent } } + // Set ids on nested header nodes + $nestedHeaders = $xPath->query('//body//*//h1|//body//*//h2|//body//*//h3|//body//*//h4|//body//*//h5|//body//*//h6'); + foreach ($nestedHeaders as $nestedHeader) { + [$oldId, $newId] = $this->setUniqueId($nestedHeader, $idMap); + if ($newId && $newId !== $oldId) { + $this->updateLinks($xPath, '#' . $oldId, '#' . $newId); + } + } + // Ensure no duplicate ids within child items $idElems = $xPath->query('//body//*//*[@id]'); foreach ($idElems as $domElem) { diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 4dace533b..9524186c8 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -670,4 +670,24 @@ class PageContentTest extends TestCase $page->refresh(); $this->assertStringContainsString('html); } + + public function test_nested_headers_gets_assigned_an_id() + { + $this->asEditor(); + $page = Page::query()->first(); + + $content = '
Simple Test
'; + $this->put($page->getUrl(), [ + 'name' => $page->name, + 'html' => $content, + 'summary' => '', + ]); + + $updatedPage = Page::query()->where('id', '=', $page->id)->first(); + + // The top level node will get assign the bkmrk-simple-test id because the system will + // take the node value of h5 + // So the h5 should get the bkmrk-simple-test-1 id + $this->assertStringContainsString('
Simple Test
', $updatedPage->html); + } } From 37db51a62761f4afc80932da5841257e51015f97 Mon Sep 17 00:00:00 2001 From: Robert Accettura Date: Sun, 21 Nov 2021 23:15:37 -0500 Subject: [PATCH 05/14] Update verify-totp.blade.php --- resources/views/mfa/parts/verify-totp.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/mfa/parts/verify-totp.blade.php b/resources/views/mfa/parts/verify-totp.blade.php index c155f1bfe..a52d9b652 100644 --- a/resources/views/mfa/parts/verify-totp.blade.php +++ b/resources/views/mfa/parts/verify-totp.blade.php @@ -6,7 +6,7 @@ {{ csrf_field() }} @if($errors->has('code')) From 9a5adc026aace8154bacecc37bb2dfe87917e4fc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 22 Nov 2021 13:28:46 +0000 Subject: [PATCH 06/14] Updated test to ensure autofocus is set on TOTP input --- tests/Auth/MfaVerificationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Auth/MfaVerificationTest.php b/tests/Auth/MfaVerificationTest.php index 49ca6663d..9a6106243 100644 --- a/tests/Auth/MfaVerificationTest.php +++ b/tests/Auth/MfaVerificationTest.php @@ -23,7 +23,7 @@ class MfaVerificationTest extends TestCase $resp = $this->get('/mfa/verify'); $resp->assertSee('Verify Access'); $resp->assertSee('Enter the code, generated using your mobile app, below:'); - $resp->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]'); + $resp->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"][autofocus]'); $google2fa = new Google2FA(); $resp = $this->post('/mfa/totp/verify', [ From cdaad2f40ec96fd98e6320638854c69b90bfe847 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 22 Nov 2021 18:30:58 +0000 Subject: [PATCH 07/14] Support custom commands via logical theme system Added initial work to support registering commands through the logical theme system. Includes docs changes and example. Not yet covered via testing. --- app/Console/Kernel.php | 10 ++++++++++ app/Theming/ThemeService.php | 22 ++++++++++++++++++++++ dev/docs/logical-theme-system.md | 26 ++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 11c8018c8..02c8c00e6 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,8 +2,11 @@ namespace BookStack\Console; +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeService; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use Symfony\Component\Console\Command\Command; class Kernel extends ConsoleKernel { @@ -35,6 +38,13 @@ class Kernel extends ConsoleKernel */ protected function commands() { + // Default framework command loading from 'Commands' directory $this->load(__DIR__ . '/Commands'); + + // Load any user commands that have been registered via the theme system. + $themeService = $this->app->make(ThemeService::class); + foreach ($themeService->getRegisteredCommands() as $command) { + $this->registerCommand($command); + } } } diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index 602abaf1c..f095c7a8e 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -3,11 +3,17 @@ namespace BookStack\Theming; use BookStack\Auth\Access\SocialAuthService; +use Symfony\Component\Console\Command\Command; class ThemeService { protected $listeners = []; + /** + * @var Command[] + */ + protected $commands = []; + /** * Listen to a given custom theme event, * setting up the action to be ran when the event occurs. @@ -43,6 +49,22 @@ class ThemeService return null; } + /** + * Register a new custom artisan command to be available. + */ + public function registerCommand(Command $command) + { + $this->commands[] = $command; + } + + /** + * Get the custom commands that have been registered. + */ + public function getRegisteredCommands(): array + { + return $this->commands; + } + /** * Read any actions from the set theme path if the 'functions.php' file exists. */ diff --git a/dev/docs/logical-theme-system.md b/dev/docs/logical-theme-system.md index b950d7df9..4d6ed719b 100644 --- a/dev/docs/logical-theme-system.md +++ b/dev/docs/logical-theme-system.md @@ -77,6 +77,32 @@ Theme::listen(ThemeEvents::APP_BOOT, function($app) { }); ``` +## Custom Commands + +The logical theme system supports adding custom [artisan commands](https://laravel.com/docs/8.x/artisan) to BookStack. These can be registered in your `functions.php` file by calling `Theme::registerCommand($command)`, where `$command` is an instance of `\Symfony\Component\Console\Command\Command`. + +Below is an example of registering a command that could then be ran using `php artisan bookstack:meow` on the command line. + +```php +line('Meow there!'); + } +} + +Theme::registerCommand(new MeowCommand); +``` + ## Custom Socialite Service Example The below shows an example of adding a custom reddit socialite service to BookStack. From cb30c258df2215ba73364cc6df9f3741f603d6a5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 22 Nov 2021 19:03:04 +0000 Subject: [PATCH 08/14] Added test for logical-theme-system command registration Changed how the command registration was handled due to complications of action order found during testing. Now the theme service will resolve and directly register the command on the Kernel instead of them being fetched from the ThemeService from within Kernel. More direct, Seems to work. --- app/Console/Kernel.php | 10 ---------- app/Theming/ThemeService.php | 18 ++++-------------- tests/ThemeTest.php | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 02c8c00e6..11c8018c8 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,11 +2,8 @@ namespace BookStack\Console; -use BookStack\Facades\Theme; -use BookStack\Theming\ThemeService; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -use Symfony\Component\Console\Command\Command; class Kernel extends ConsoleKernel { @@ -38,13 +35,6 @@ class Kernel extends ConsoleKernel */ protected function commands() { - // Default framework command loading from 'Commands' directory $this->load(__DIR__ . '/Commands'); - - // Load any user commands that have been registered via the theme system. - $themeService = $this->app->make(ThemeService::class); - foreach ($themeService->getRegisteredCommands() as $command) { - $this->registerCommand($command); - } } } diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index f095c7a8e..f0f8f033c 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -3,17 +3,13 @@ namespace BookStack\Theming; use BookStack\Auth\Access\SocialAuthService; +use Illuminate\Contracts\Console\Kernel; use Symfony\Component\Console\Command\Command; class ThemeService { protected $listeners = []; - /** - * @var Command[] - */ - protected $commands = []; - /** * Listen to a given custom theme event, * setting up the action to be ran when the event occurs. @@ -54,15 +50,9 @@ class ThemeService */ public function registerCommand(Command $command) { - $this->commands[] = $command; - } - - /** - * Get the custom commands that have been registered. - */ - public function getRegisteredCommands(): array - { - return $this->commands; + /** @var \Illuminate\Foundation\Console\Kernel $consoleKernel */ + $consoleKernel = app()->make(Kernel::class); + $consoleKernel->registerCommand($command); } /** diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index 9aa7873b0..f04250bff 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -7,8 +7,10 @@ use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\PageContent; use BookStack\Facades\Theme; use BookStack\Theming\ThemeEvents; +use Illuminate\Console\Command; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\File; use League\CommonMark\ConfigurableEnvironmentInterface; @@ -206,6 +208,16 @@ class ThemeTest extends TestCase $this->assertStringContainsString('donkey=donut', $redirect); } + public function test_register_command_allows_provided_command_to_be_usable_via_artisan() + { + Theme::registerCommand(new MyCustomCommand); + + Artisan::call('bookstack:test-custom-command', []); + $output = Artisan::output(); + + $this->assertStringContainsString('Command ran!', $output); + } + protected function usingThemeFolder(callable $callback) { // Create a folder and configure a theme @@ -220,3 +232,10 @@ class ThemeTest extends TestCase File::deleteDirectory($themeFolderPath); } } + +class MyCustomCommand extends Command { + protected $signature = 'bookstack:test-custom-command'; + public function handle() { + $this->line('Command ran!'); + } +} \ No newline at end of file From 1bf59f434bad1496d207abc340fb5524a9e78ff9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 22 Nov 2021 22:22:31 +0000 Subject: [PATCH 09/14] Tweaked custom command registration, Added StyleCI fixes Old command registration method was interfering with default commands, causing only a limited subset of commands to show overall. This change follows the method the frameworks uses when loading in from a directory to prevent issues with run/load order. --- app/Auth/Access/ExternalBaseUserProvider.php | 4 ++-- app/Entities/Models/Entity.php | 1 + app/Interfaces/Deletable.php | 2 +- app/Theming/ThemeService.php | 8 +++++--- tests/ThemeTest.php | 11 +++++++---- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/Auth/Access/ExternalBaseUserProvider.php b/app/Auth/Access/ExternalBaseUserProvider.php index dc765ddc5..122425c11 100644 --- a/app/Auth/Access/ExternalBaseUserProvider.php +++ b/app/Auth/Access/ExternalBaseUserProvider.php @@ -64,7 +64,7 @@ class ExternalBaseUserProvider implements UserProvider * Update the "remember me" token for the given user in storage. * * @param Authenticatable $user - * @param string $token + * @param string $token * * @return void */ @@ -94,7 +94,7 @@ class ExternalBaseUserProvider implements UserProvider * Validate a user against the given credentials. * * @param Authenticatable $user - * @param array $credentials + * @param array $credentials * * @return bool */ diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index eccec40b5..2b6f85d24 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -211,6 +211,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable /** * Check if this instance or class is a certain type of entity. * Examples of $type are 'page', 'book', 'chapter'. + * * @deprecated Use instanceof instead. */ public static function isA(string $type): bool diff --git a/app/Interfaces/Deletable.php b/app/Interfaces/Deletable.php index ca59e04f3..be9b4ac41 100644 --- a/app/Interfaces/Deletable.php +++ b/app/Interfaces/Deletable.php @@ -11,4 +11,4 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; interface Deletable { public function deletions(): MorphMany; -} \ No newline at end of file +} diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index f0f8f033c..7bc12c5d1 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -3,6 +3,8 @@ namespace BookStack\Theming; use BookStack\Auth\Access\SocialAuthService; +use Illuminate\Console\Application; +use Illuminate\Console\Application as Artisan; use Illuminate\Contracts\Console\Kernel; use Symfony\Component\Console\Command\Command; @@ -50,9 +52,9 @@ class ThemeService */ public function registerCommand(Command $command) { - /** @var \Illuminate\Foundation\Console\Kernel $consoleKernel */ - $consoleKernel = app()->make(Kernel::class); - $consoleKernel->registerCommand($command); + Artisan::starting(function(Application $application) use ($command) { + $application->addCommands([$command]); + }); } /** diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index f04250bff..364bf6900 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -210,7 +210,7 @@ class ThemeTest extends TestCase public function test_register_command_allows_provided_command_to_be_usable_via_artisan() { - Theme::registerCommand(new MyCustomCommand); + Theme::registerCommand(new MyCustomCommand()); Artisan::call('bookstack:test-custom-command', []); $output = Artisan::output(); @@ -233,9 +233,12 @@ class ThemeTest extends TestCase } } -class MyCustomCommand extends Command { +class MyCustomCommand extends Command +{ protected $signature = 'bookstack:test-custom-command'; - public function handle() { + + public function handle() + { $this->line('Command ran!'); } -} \ No newline at end of file +} From 024924eef38179ecb12ef5cd6d7bcdcb8c15a298 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 22 Nov 2021 23:33:55 +0000 Subject: [PATCH 10/14] Applied another round of static analysis updates --- app/Actions/ActivityService.php | 4 +-- app/Auth/Access/LdapService.php | 2 +- app/Auth/Access/Oidc/OidcJwtSigningKey.php | 19 +++++++---- app/Auth/Access/SocialAuthService.php | 3 +- app/Entities/Models/Book.php | 22 ++++--------- app/Entities/Models/Bookshelf.php | 2 +- app/Entities/Models/Chapter.php | 4 ++- app/Entities/Models/Entity.php | 4 +-- app/Entities/Models/PageRevision.php | 4 +-- app/Entities/Repos/PageRepo.php | 5 +-- app/Entities/Tools/PageContent.php | 8 +++-- app/Entities/Tools/SearchIndex.php | 7 ++-- app/Entities/Tools/SearchRunner.php | 4 +-- app/Entities/Tools/TrashCan.php | 18 +++++------ .../Api/BookshelfApiController.php | 2 +- .../Controllers/Api/ChapterApiController.php | 2 +- app/Http/Controllers/BookController.php | 2 +- app/Theming/ThemeService.php | 2 +- app/Uploads/ImageRepo.php | 5 ++- tests/Unit/FrameworkAssumptionTest.php | 32 +++++++++++++++++++ version | 2 +- 21 files changed, 96 insertions(+), 57 deletions(-) create mode 100644 tests/Unit/FrameworkAssumptionTest.php diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php index 33ed44b32..983c1a603 100644 --- a/app/Actions/ActivityService.php +++ b/app/Actions/ActivityService.php @@ -105,10 +105,10 @@ class ActivityService $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(); diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index e3a38537a..e529b80fd 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -165,7 +165,7 @@ class LdapService * Bind the system user to the LDAP connection using the given credentials * otherwise anonymous access is attempted. * - * @param $connection + * @param resource $connection * * @throws LdapException */ diff --git a/app/Auth/Access/Oidc/OidcJwtSigningKey.php b/app/Auth/Access/Oidc/OidcJwtSigningKey.php index 9a5b3833a..a70f3b3c7 100644 --- a/app/Auth/Access/Oidc/OidcJwtSigningKey.php +++ b/app/Auth/Access/Oidc/OidcJwtSigningKey.php @@ -41,16 +41,18 @@ class OidcJwtSigningKey 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); } /** @@ -81,14 +83,19 @@ class OidcJwtSigningKey $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); } /** diff --git a/app/Auth/Access/SocialAuthService.php b/app/Auth/Access/SocialAuthService.php index 23e95970c..0df5ceb5e 100644 --- a/app/Auth/Access/SocialAuthService.php +++ b/app/Auth/Access/SocialAuthService.php @@ -12,6 +12,7 @@ use Illuminate\Support\Str; 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; @@ -278,7 +279,7 @@ class SocialAuthService { $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']); } diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 735d25a99..8217d2cab 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -79,53 +79,43 @@ class Book extends Entity implements HasCoverImage /** * 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'); } diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index e4d9775b7..b9ebab92e 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -37,7 +37,7 @@ class Bookshelf extends Entity implements HasCoverImage */ public function visibleBooks(): BelongsToMany { - return $this->books()->visible(); + return $this->books()->scopes('visible'); } /** diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index 224ded935..8fc2d333d 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -23,6 +23,7 @@ class Chapter extends BookChild /** * Get the pages that this chapter contains. + * @return HasMany */ public function pages(string $dir = 'ASC'): HasMany { @@ -50,7 +51,8 @@ class Chapter extends BookChild */ public function getVisiblePages(): Collection { - return $this->pages()->visible() + return $this->pages() + ->scopes('visible') ->orderBy('draft', 'desc') ->orderBy('priority', 'asc') ->get(); diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 2b6f85d24..0eb402284 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -121,11 +121,11 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable 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; } diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php index a3c4379a6..2bfa169f4 100644 --- a/app/Entities/Models/PageRevision.php +++ b/app/Entities/Models/PageRevision.php @@ -63,10 +63,8 @@ class PageRevision extends Model /** * 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) diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index ed5534f08..24fc1e7dd 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -69,9 +69,10 @@ class PageRepo */ 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') @@ -80,7 +81,7 @@ class PageRepo ->with('page') ->first(); - return $revision ? $revision->page : null; + return $revision->page ?? null; } /** diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index c60cf0311..b95131fce 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -12,6 +12,8 @@ use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageService; use BookStack\Util\HtmlContentFilter; use DOMDocument; +use DOMElement; +use DOMNode; use DOMNodeList; use DOMXPath; use Illuminate\Support\Str; @@ -237,9 +239,9 @@ class PageContent * 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 ['', '']; } @@ -321,7 +323,7 @@ class PageContent */ 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); diff --git a/app/Entities/Tools/SearchIndex.php b/app/Entities/Tools/SearchIndex.php index 79139ec24..d43d98207 100644 --- a/app/Entities/Tools/SearchIndex.php +++ b/app/Entities/Tools/SearchIndex.php @@ -9,6 +9,7 @@ use BookStack\Entities\Models\Page; use BookStack\Entities\Models\SearchTerm; use DOMDocument; use DOMNode; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; class SearchIndex @@ -76,7 +77,9 @@ 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 $query */ + $query = $entityModel->newQuery(); + $total = $query->withTrashed()->count(); $chunkSize = 250; $processed = 0; @@ -223,7 +226,7 @@ class SearchIndex 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); diff --git a/app/Entities/Tools/SearchRunner.php b/app/Entities/Tools/SearchRunner.php index a4f131482..a0a44f3a5 100644 --- a/app/Entities/Tools/SearchRunner.php +++ b/app/Entities/Tools/SearchRunner.php @@ -145,13 +145,13 @@ class SearchRunner 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'); }; } diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index fcf933726..ab62165af 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -15,6 +15,7 @@ use BookStack\Facades\Activity; use BookStack\Uploads\AttachmentService; use BookStack\Uploads\ImageService; use Exception; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; class TrashCan @@ -141,11 +142,9 @@ 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); @@ -183,9 +182,10 @@ class TrashCan { $counts = []; - /** @var Entity $instance */ foreach ((new EntityProvider())->all() as $key => $instance) { - $counts[$key] = $instance->newQuery()->onlyTrashed()->count(); + /** @var Builder $query */ + $query = $instance->newQuery(); + $counts[$key] = $query->onlyTrashed()->count(); } return $counts; @@ -344,9 +344,9 @@ class TrashCan $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()); } } } diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index de7284e61..bd4f23a10 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -75,7 +75,7 @@ class BookshelfApiController extends ApiController $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); diff --git a/app/Http/Controllers/Api/ChapterApiController.php b/app/Http/Controllers/Api/ChapterApiController.php index 6b226b5f0..8459b8449 100644 --- a/app/Http/Controllers/Api/ChapterApiController.php +++ b/app/Http/Controllers/Api/ChapterApiController.php @@ -70,7 +70,7 @@ class ChapterApiController extends ApiController 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); diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index af44a6689..51cba642c 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -114,7 +114,7 @@ class BookController extends Controller { $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')) { diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index 7bc12c5d1..57336ec5f 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -52,7 +52,7 @@ class ThemeService */ public function registerCommand(Command $command) { - Artisan::starting(function(Application $application) use ($command) { + Artisan::starting(function (Application $application) use ($command) { $application->addCommands([$command]); }); } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 494ff3ac0..bfe4b5977 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -103,7 +103,10 @@ class ImageRepo 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); } }; diff --git a/tests/Unit/FrameworkAssumptionTest.php b/tests/Unit/FrameworkAssumptionTest.php new file mode 100644 index 000000000..8ad45612c --- /dev/null +++ b/tests/Unit/FrameworkAssumptionTest.php @@ -0,0 +1,32 @@ +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); + } +} diff --git a/version b/version index 4a22367ef..8f6af9bee 100644 --- a/version +++ b/version @@ -1 +1 @@ -v21.10-dev +v21.11-dev From 096ed722dd4cc554fd87943b063c8b61f6e0dac6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 23 Nov 2021 15:48:55 +0000 Subject: [PATCH 11/14] Added use of the prefers-contrast media query Used upon areas we usually fade-out to provide a focused user experience. If the user desires more contrasted we prevent this behaviour using the prefers-contrast media query. Related to #2634 --- resources/sass/_blocks.scss | 3 +++ resources/sass/_header.scss | 3 +++ resources/sass/_layout.scss | 3 +++ resources/sass/_pages.scss | 3 +++ 4 files changed, 12 insertions(+) diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss index ae3e7a441..b7aea7452 100644 --- a/resources/sass/_blocks.scss +++ b/resources/sass/_blocks.scss @@ -229,6 +229,9 @@ &:hover, &:focus-within { opacity: 1; } + @media (prefers-contrast: more) { + opacity: 1; + } } /** diff --git a/resources/sass/_header.scss b/resources/sass/_header.scss index 1a7015078..f070f5a18 100644 --- a/resources/sass/_header.scss +++ b/resources/sass/_header.scss @@ -262,6 +262,9 @@ header .search-box { &:hover, &:focus-within { opacity: 1; } + @media (prefers-contrast: more) { + opacity: 1; + } } @include smaller-than($l) { diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 362bab7d3..783ccc8f9 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -369,6 +369,9 @@ body.flexbox { &:focus-within { opacity: 1; } + @media (prefers-contrast: more) { + opacity: 1; + } } } diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 4f249244b..14c253679 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -412,4 +412,7 @@ body.mce-fullscreen, body.markdown-fullscreen { text-decoration: none; opacity: 1; } + @media (prefers-contrast: more) { + opacity: 1; + } } \ No newline at end of file From 197caddf96b3d9ce6073109cfc2289133dd456f9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 23 Nov 2021 18:18:49 +0000 Subject: [PATCH 12/14] Changed homepage card header links to be bottom-card-links The old links in the headers were not obvious. This changes the header-based links to instead be a link at the bottom of the card. Related to #3046 --- app/Http/Controllers/HomeController.php | 2 +- resources/sass/_blocks.scss | 14 ++++++++++++++ resources/views/home/default.blade.php | 10 +++++----- resources/views/home/parts/sidebar.blade.php | 8 ++++---- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 5451c0abf..df810a3cf 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -39,7 +39,7 @@ class HomeController extends Controller $recentlyUpdatedPages = Page::visible()->with('book') ->where('draft', false) ->orderBy('updated_at', 'desc') - ->take($favourites->count() > 0 ? 6 : 12) + ->take($favourites->count() > 0 ? 5 : 10) ->select(Page::$listAttributes) ->get(); diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss index b7aea7452..0a7a689f7 100644 --- a/resources/sass/_blocks.scss +++ b/resources/sass/_blocks.scss @@ -87,6 +87,20 @@ .card-title a { line-height: 1; } +.card-footer-link { + display: block; + padding: $-s $-m; + line-height: 1; + border-top: 1px solid; + @include lightDark(border-color, #DDD, #555); + border-radius: 0 0 3px 3px; + font-size: 0.9em; + margin-top: $-xs; + &:hover { + text-decoration: none; + @include lightDark(background-color, #f2f2f2, #2d2d2d); + } +} .card.border-card { border: 1px solid #DDD; diff --git a/resources/views/home/default.blade.php b/resources/views/home/default.blade.php index b8866526d..f6a337e50 100644 --- a/resources/views/home/default.blade.php +++ b/resources/views/home/default.blade.php @@ -44,27 +44,27 @@
@if(count($favourites) > 0)
-

- {{ trans('entities.my_most_viewed_favourites') }} -

+

{{ trans('entities.my_most_viewed_favourites') }}

@include('entities.list', [ 'entities' => $favourites, 'style' => 'compact', ])
+ {{ trans('common.view_all') }}
@endif
-

{{ trans('entities.recently_updated_pages') }}

+

{{ trans('entities.recently_updated_pages') }}

@include('entities.list', [ 'entities' => $recentlyUpdatedPages, 'style' => 'compact', - 'emptyText' => trans('entities.no_pages_recently_updated') + 'emptyText' => trans('entities.no_pages_recently_updated'), ])
+ {{ trans('common.view_all') }}
diff --git a/resources/views/home/parts/sidebar.blade.php b/resources/views/home/parts/sidebar.blade.php index 8dc8118f5..78f7e7a80 100644 --- a/resources/views/home/parts/sidebar.blade.php +++ b/resources/views/home/parts/sidebar.blade.php @@ -7,13 +7,12 @@ @if(count($favourites) > 0)
-
- {{ trans('entities.my_most_viewed_favourites') }} -
+
{{ trans('entities.my_most_viewed_favourites') }}
@include('entities.list', [ 'entities' => $favourites, 'style' => 'compact', ]) + {{ trans('common.view_all') }}
@endif @@ -27,7 +26,7 @@
-
{{ trans('entities.recently_updated_pages') }}
+
{{ trans('entities.recently_updated_pages') }}
@include('entities.list', [ 'entities' => $recentlyUpdatedPages, @@ -35,6 +34,7 @@ 'emptyText' => trans('entities.no_pages_recently_updated') ])
+ {{ trans('common.view_all') }}
From f63d7f60aa3af313cf984f215383368c0fb19623 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 23 Nov 2021 20:38:52 +0000 Subject: [PATCH 13/14] New Crowdin updates (#3057) * New translations auth.php (Chinese Simplified) * New translations common.php (Chinese Simplified) * New translations entities.php (Chinese Simplified) * New translations common.php (Chinese Simplified) * New translations settings.php (Chinese Simplified) * New translations activities.php (Chinese Simplified) * New translations entities.php (Chinese Simplified) * New translations auth.php (Russian) * New translations common.php (Russian) * New translations common.php (Russian) * New translations entities.php (Russian) * New translations errors.php (Russian) * New translations settings.php (Russian) * New translations auth.php (Italian) * New translations common.php (Italian) * New translations entities.php (Italian) * New translations entities.php (Italian) * New translations auth.php (Estonian) --- resources/lang/et/auth.php | 4 ++-- resources/lang/it/auth.php | 4 ++-- resources/lang/it/common.php | 4 ++-- resources/lang/it/entities.php | 18 +++++++++--------- resources/lang/ru/auth.php | 4 ++-- resources/lang/ru/common.php | 6 +++--- resources/lang/ru/entities.php | 26 +++++++++++++------------- resources/lang/ru/errors.php | 8 ++++---- resources/lang/ru/settings.php | 10 +++++----- resources/lang/zh_CN/activities.php | 4 ++-- resources/lang/zh_CN/auth.php | 8 ++++---- resources/lang/zh_CN/common.php | 6 +++--- resources/lang/zh_CN/entities.php | 24 ++++++++++++------------ resources/lang/zh_CN/settings.php | 4 ++-- 14 files changed, 65 insertions(+), 65 deletions(-) diff --git a/resources/lang/et/auth.php b/resources/lang/et/auth.php index b23e316c8..934d1847d 100644 --- a/resources/lang/et/auth.php +++ b/resources/lang/et/auth.php @@ -54,7 +54,7 @@ return [ 'email_confirm_text' => 'Palun kinnita oma e-posti aadress, klõpsates alloleval nupul:', 'email_confirm_action' => 'Kinnita e-posti aadress', 'email_confirm_send_error' => 'E-posti aadressi kinnitamine on vajalik, aga e-kirja saatmine ebaõnnestus. Võta ühendust administraatoriga.', - 'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.', + 'email_confirm_success' => 'E-posti aadress on kinnitatud! Nüüd saad selle aadressiga sisse logida.', 'email_confirm_resent' => 'Kinnituskiri on saadetud, vaata oma postkasti.', 'email_not_confirmed' => 'E-posti aadress ei ole kinnitatud', @@ -71,7 +71,7 @@ return [ 'user_invite_page_welcome' => 'Tere tulemast rakendusse :appName!', 'user_invite_page_text' => 'Registreerumise lõpetamiseks ja ligipääsu saamiseks pead seadma parooli, millega edaspidi rakendusse sisse logid.', 'user_invite_page_confirm_button' => 'Kinnita parool', - 'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!', + 'user_invite_success_login' => 'Parool seatud, nüüd on sul selle parooli abil ligipääs rakendusele :appName!', // Multi-factor Authentication 'mfa_setup' => 'Seadista mitmeastmeline autentimine', diff --git a/resources/lang/it/auth.php b/resources/lang/it/auth.php index 40d604752..346a12611 100755 --- a/resources/lang/it/auth.php +++ b/resources/lang/it/auth.php @@ -54,7 +54,7 @@ return [ 'email_confirm_text' => 'Conferma il tuo indirizzo email cliccando il pulsante sotto:', 'email_confirm_action' => 'Conferma Email', 'email_confirm_send_error' => 'La conferma della mail è richiesta ma non è stato possibile mandare la mail. Contatta l\'amministratore.', - 'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.', + 'email_confirm_success' => 'La tua email è stata confermata! Ora dovresti essere in grado di effettuare il login utilizzando questo indirizzo email.', 'email_confirm_resent' => 'Mail di conferma reinviata, controlla la tua posta.', 'email_not_confirmed' => 'Indirizzo Email Non Confermato', @@ -71,7 +71,7 @@ return [ 'user_invite_page_welcome' => 'Benvenuto in :appName!', 'user_invite_page_text' => 'Per completare il tuo account e ottenere l\'accesso devi impostare una password che verrà utilizzata per accedere a :appName in futuro.', 'user_invite_page_confirm_button' => 'Conferma Password', - 'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!', + 'user_invite_success_login' => 'Password impostata, ora dovresti essere in grado di effettuare il login utilizzando la password impostata per accedere a :appName!', // Multi-factor Authentication 'mfa_setup' => 'Imposta Autenticazione Multi-Fattore', diff --git a/resources/lang/it/common.php b/resources/lang/it/common.php index e8ef850c6..07d0b4574 100755 --- a/resources/lang/it/common.php +++ b/resources/lang/it/common.php @@ -45,8 +45,8 @@ return [ 'unfavourite' => 'Rimuovi dai preferiti', 'next' => 'Successivo', 'previous' => 'Precedente', - 'filter_active' => 'Active Filter:', - 'filter_clear' => 'Clear Filter', + 'filter_active' => 'Filtro attivo:', + 'filter_clear' => 'Pulisci filtro', // Sort Options 'sort_options' => 'Opzioni Ordinamento', diff --git a/resources/lang/it/entities.php b/resources/lang/it/entities.php index 273551d96..97a6a88a5 100755 --- a/resources/lang/it/entities.php +++ b/resources/lang/it/entities.php @@ -258,16 +258,16 @@ return [ 'tags_explain' => "Aggiungi tag per categorizzare meglio il contenuto. \n Puoi assegnare un valore ai tag per una migliore organizzazione.", 'tags_add' => 'Aggiungi un altro tag', 'tags_remove' => 'Rimuovi questo tag', - 'tags_usages' => 'Total tag usages', - 'tags_assigned_pages' => 'Assigned to Pages', - 'tags_assigned_chapters' => 'Assigned to Chapters', - 'tags_assigned_books' => 'Assigned to Books', - 'tags_assigned_shelves' => 'Assigned to Shelves', + 'tags_usages' => 'Utilizzo totale dei tag', + 'tags_assigned_pages' => 'Assegnato alle Pagine', + 'tags_assigned_chapters' => 'Assegnato ai capitoli', + 'tags_assigned_books' => 'Assegnato a Libri', + 'tags_assigned_shelves' => 'Assegnato alle Librerie', 'tags_x_unique_values' => ':count unique values', - 'tags_all_values' => 'All values', - 'tags_view_tags' => 'View Tags', - 'tags_view_existing_tags' => 'View existing tags', - 'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.', + 'tags_all_values' => 'Tutti i valori', + 'tags_view_tags' => 'Visualizza tag', + 'tags_view_existing_tags' => 'Usa i tag esistenti', + 'tags_list_empty_hint' => 'I tag possono essere assegnati tramite la barra laterale dell\'editor di pagina o durante la modifica dei dettagli di un libro, capitolo o libreria.', 'attachments' => 'Allegati', 'attachments_explain' => 'Carica alcuni file o allega link per visualizzarli nella pagina. Questi sono visibili nella sidebar della pagina.', 'attachments_explain_instant_save' => 'I cambiamenti qui sono salvati istantaneamente.', diff --git a/resources/lang/ru/auth.php b/resources/lang/ru/auth.php index 97402eb69..bb05c70e1 100644 --- a/resources/lang/ru/auth.php +++ b/resources/lang/ru/auth.php @@ -54,7 +54,7 @@ return [ 'email_confirm_text' => 'Пожалуйста, подтвердите свой адрес электронной почты нажав на кнопку ниже:', 'email_confirm_action' => 'Подтвердить адрес электронной почты', 'email_confirm_send_error' => 'Требуется подтверждение электронной почты, но система не может отправить письмо. Свяжитесь с администратором, чтобы убедиться, что адрес электронной почты настроен правильно.', - 'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.', + 'email_confirm_success' => 'Ваш адрес электронной почты был подтвержден! Теперь вы можете войти в систему, используя этот адрес электронной почты.', 'email_confirm_resent' => 'Письмо с подтверждение выслано снова. Пожалуйста, проверьте ваш почтовый ящик.', 'email_not_confirmed' => 'Адрес электронной почты не подтвержден', @@ -71,7 +71,7 @@ return [ 'user_invite_page_welcome' => 'Добро пожаловать в :appName!', 'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.', 'user_invite_page_confirm_button' => 'Подтвердите пароль', - 'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!', + 'user_invite_success_login' => 'Пароль установлен, теперь вы можете войти в систему, используя установленный пароль для доступа к :appName!', // Multi-factor Authentication 'mfa_setup' => 'Двухфакторная аутентификация', diff --git a/resources/lang/ru/common.php b/resources/lang/ru/common.php index aac4c84d0..377281957 100644 --- a/resources/lang/ru/common.php +++ b/resources/lang/ru/common.php @@ -39,14 +39,14 @@ return [ 'reset' => 'Сбросить', 'remove' => 'Удалить', 'add' => 'Добавить', - 'configure' => 'Configure', + 'configure' => 'Настройка', 'fullscreen' => 'На весь экран', 'favourite' => 'Избранное', 'unfavourite' => 'Убрать из избранного', 'next' => 'Следующая', 'previous' => 'Предыдущая', - 'filter_active' => 'Active Filter:', - 'filter_clear' => 'Clear Filter', + 'filter_active' => 'Активный фильтр:', + 'filter_clear' => 'Сбросить фильтр', // Sort Options 'sort_options' => 'Параметры сортировки', diff --git a/resources/lang/ru/entities.php b/resources/lang/ru/entities.php index 39ba10c6b..fedeebc91 100644 --- a/resources/lang/ru/entities.php +++ b/resources/lang/ru/entities.php @@ -36,7 +36,7 @@ return [ 'export_html' => 'Веб файл', 'export_pdf' => 'PDF файл', 'export_text' => 'Текстовый файл', - 'export_md' => 'Markdown File', + 'export_md' => 'Файл Markdown', // Permissions and restrictions 'permissions' => 'Разрешения', @@ -99,7 +99,7 @@ return [ 'shelves_permissions' => 'Доступы к книжной полке', 'shelves_permissions_updated' => 'Доступы к книжной полке обновлены', 'shelves_permissions_active' => 'Действующие разрешения книжной полки', - 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', + 'shelves_permissions_cascade_warning' => 'Разрешения на полки не наследуются автоматически содержащимся в них книгам. Это происходит потому, что книга может находиться на нескольких полках. Однако разрешения могут быть установлены для книг полки с помощью опции, приведенной ниже.', 'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам', 'shelves_copy_permissions' => 'Копировать доступы', 'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.', @@ -234,7 +234,7 @@ return [ 'pages_initial_name' => 'Новая страница', 'pages_editing_draft_notification' => 'В настоящее время вы редактируете черновик, который был сохранён :timeDiff.', 'pages_draft_edited_notification' => 'Эта страница была обновлена до этого момента. Рекомендуется отменить этот черновик.', - 'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.', + 'pages_draft_page_changed_since_creation' => 'Эта страница была обновлена с момента создания данного черновика. Рекомендуется выбросить этот черновик или следить за тем, чтобы не перезаписать все изменения на странице.', 'pages_draft_edit_active' => [ 'start_a' => ':count пользователей начали редактирование этой страницы', 'start_b' => ':userName начал редактирование этой страницы', @@ -258,16 +258,16 @@ return [ 'tags_explain' => "Добавьте теги, чтобы лучше классифицировать ваш контент. \\n Вы можете присвоить значение тегу для более глубокой организации.", 'tags_add' => 'Добавить тег', 'tags_remove' => 'Удалить этот тег', - 'tags_usages' => 'Total tag usages', - 'tags_assigned_pages' => 'Assigned to Pages', - 'tags_assigned_chapters' => 'Assigned to Chapters', - 'tags_assigned_books' => 'Assigned to Books', - 'tags_assigned_shelves' => 'Assigned to Shelves', - 'tags_x_unique_values' => ':count unique values', - 'tags_all_values' => 'All values', - 'tags_view_tags' => 'View Tags', - 'tags_view_existing_tags' => 'View existing tags', - 'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.', + 'tags_usages' => 'Всего использовано тегов', + 'tags_assigned_pages' => 'Назначено на страницы', + 'tags_assigned_chapters' => 'Назначено на главы', + 'tags_assigned_books' => 'Назначено на книги', + 'tags_assigned_shelves' => 'Назначено на полки', + 'tags_x_unique_values' => 'Уникальные значения: :count', + 'tags_all_values' => 'Все значения', + 'tags_view_tags' => 'Посмотреть теги', + 'tags_view_existing_tags' => 'Просмотр имеющихся тегов', + 'tags_list_empty_hint' => 'Теги можно присваивать через боковую панель редактора страниц или при редактировании сведений о книге, главе или полке.', 'attachments' => 'Вложения', 'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.', 'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.', diff --git a/resources/lang/ru/errors.php b/resources/lang/ru/errors.php index 1edef426c..7ea17aa72 100644 --- a/resources/lang/ru/errors.php +++ b/resources/lang/ru/errors.php @@ -23,10 +23,10 @@ return [ 'saml_no_email_address' => 'Не удалось найти email для этого пользователя в данных, предоставленных внешней системой аутентификации', 'saml_invalid_response_id' => 'Запрос от внешней системы аутентификации не распознается процессом, запущенным этим приложением. Переход назад после входа в систему может вызвать эту проблему.', 'saml_fail_authed' => 'Вход с помощью :system не удался, система не предоставила успешную авторизацию', - 'oidc_already_logged_in' => 'Already logged in', - 'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled', - 'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', - 'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization', + 'oidc_already_logged_in' => 'Вход в систему уже произведен', + 'oidc_user_not_registered' => 'Пользователь :name не зарегистрирован и автоматическая регистрация отключена', + 'oidc_no_email_address' => 'Не удалось найти email этого пользователя в данных, предоставленных внешней системой аутентификации', + 'oidc_fail_authed' => 'Вход в систему с помощью :system не удался, система не обеспечила успешную авторизацию', 'social_no_action_defined' => 'Действие не определено', 'social_login_bad_response' => "При попытке входа с :socialAccount произошла ошибка: \\n:error", 'social_account_in_use' => 'Этот :socialAccount аккаунт уже используется, попробуйте войти с параметрами :socialAccount.', diff --git a/resources/lang/ru/settings.php b/resources/lang/ru/settings.php index af18213ce..9b6bb326b 100755 --- a/resources/lang/ru/settings.php +++ b/resources/lang/ru/settings.php @@ -92,7 +92,7 @@ return [ 'recycle_bin' => 'Корзина', 'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.', 'recycle_bin_deleted_item' => 'Удаленный элемент', - 'recycle_bin_deleted_parent' => 'Parent', + 'recycle_bin_deleted_parent' => 'Родительский объект', 'recycle_bin_deleted_by' => 'Удалён', 'recycle_bin_deleted_at' => 'Время удаления', 'recycle_bin_permanently_delete' => 'Удалить навсегда', @@ -105,7 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Элементы для восстановления', 'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.', 'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.', - 'recycle_bin_restore_parent' => 'Restore Parent', + 'recycle_bin_restore_parent' => 'Восстановить родительский объект', 'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.', 'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины', @@ -139,7 +139,7 @@ return [ 'role_details' => 'Детали роли', 'role_name' => 'Название роли', 'role_desc' => 'Краткое описание роли', - 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', + 'role_mfa_enforced' => 'Требует многофакторной аутентификации', 'role_external_auth_id' => 'Внешние ID авторизации', 'role_system' => 'Системные разрешения', 'role_manage_users' => 'Управление пользователями', @@ -149,7 +149,7 @@ return [ 'role_manage_page_templates' => 'Управление шаблонами страниц', 'role_access_api' => 'Доступ к системному API', 'role_manage_settings' => 'Управление настройками приложения', - 'role_export_content' => 'Export content', + 'role_export_content' => 'Экспорт контента', 'role_asset' => 'Права доступа к материалам', 'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.', 'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.', @@ -209,7 +209,7 @@ return [ 'users_api_tokens_docs' => 'Документация', 'users_mfa' => 'Двухфакторная аутентификация', 'users_mfa_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.', - 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_x_methods' => 'методов настроено :count|методов сконфигурировано :count', 'users_mfa_configure' => 'Настройка методов', // API Tokens diff --git a/resources/lang/zh_CN/activities.php b/resources/lang/zh_CN/activities.php index 65e7c3583..82c6e8f26 100644 --- a/resources/lang/zh_CN/activities.php +++ b/resources/lang/zh_CN/activities.php @@ -44,8 +44,8 @@ return [ 'bookshelf_delete_notification' => '书架已成功删除', // Favourites - 'favourite_add_notification' => '":name" 已添加到你的收藏', - 'favourite_remove_notification' => '":name" 已从你的收藏中删除', + 'favourite_add_notification' => '":name" 已添加到您的收藏', + 'favourite_remove_notification' => '":name" 已从您的收藏中删除', // MFA 'mfa_setup_method_notification' => '多重身份认证设置成功', diff --git a/resources/lang/zh_CN/auth.php b/resources/lang/zh_CN/auth.php index ea97aaa07..201fe2500 100644 --- a/resources/lang/zh_CN/auth.php +++ b/resources/lang/zh_CN/auth.php @@ -54,7 +54,7 @@ return [ 'email_confirm_text' => '请点击下面的按钮确认您的Email地址:', 'email_confirm_action' => '确认Email', 'email_confirm_send_error' => '需要Email验证,但系统无法发送电子邮件,请联系网站管理员。', - 'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.', + 'email_confirm_success' => '您已成功验证电子邮件地址!您现在可以使用此电子邮件地址登录。', 'email_confirm_resent' => '验证邮件已重新发送,请检查收件箱。', 'email_not_confirmed' => 'Email地址未验证', @@ -71,7 +71,7 @@ return [ 'user_invite_page_welcome' => '欢迎来到 :appName!', 'user_invite_page_text' => '要完成您的帐户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。', 'user_invite_page_confirm_button' => '确认密码', - 'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!', + 'user_invite_success_login' => '密码已设置,您现在可以使用您设置的密码登录 :appName!', // Multi-factor Authentication 'mfa_setup' => '设置多重身份认证', @@ -92,7 +92,7 @@ return [ 'mfa_gen_backup_codes_usage_warning' => '每个认证码只能使用一次', 'mfa_gen_totp_title' => '移动设备 App', 'mfa_gen_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。', - 'mfa_gen_totp_scan' => '要开始操作,请使用你的身份验证 App 扫描下面的二维码。', + 'mfa_gen_totp_scan' => '要开始操作,请使用您的身份验证 App 扫描下面的二维码。', 'mfa_gen_totp_verify_setup' => '验证设置', 'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的认证码来验证一切是否正常:', 'mfa_gen_totp_provide_code_here' => '在此输入您的 App 生成的认证码', @@ -103,7 +103,7 @@ return [ 'mfa_verify_use_totp' => '使用移动设备 App 进行认证', 'mfa_verify_use_backup_codes' => '使用备用认证码进行认证', 'mfa_verify_backup_code' => '备用认证码', - 'mfa_verify_backup_code_desc' => '在下面输入你的其中一个备用认证码:', + 'mfa_verify_backup_code_desc' => '在下面输入您的其中一个备用认证码:', 'mfa_verify_backup_code_enter_here' => '在这里输入备用认证码', 'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的认证码:', 'mfa_setup_login_notification' => '多重身份认证已设置,请使用新配置的方法重新登录。', diff --git a/resources/lang/zh_CN/common.php b/resources/lang/zh_CN/common.php index 5b45ee450..1201178f0 100644 --- a/resources/lang/zh_CN/common.php +++ b/resources/lang/zh_CN/common.php @@ -19,7 +19,7 @@ return [ 'description' => '概要', 'role' => '角色', 'cover_image' => '封面图片', - 'cover_image_description' => '该图像大小需要为440x250px。', + 'cover_image_description' => '此图像大小应约为 440x250 像素。', // Actions 'actions' => '操作', @@ -45,8 +45,8 @@ return [ 'unfavourite' => '取消收藏', 'next' => '下一页', 'previous' => '上一页', - 'filter_active' => 'Active Filter:', - 'filter_clear' => 'Clear Filter', + 'filter_active' => '标签过滤器:', + 'filter_clear' => '清除过滤器', // Sort Options 'sort_options' => '排序选项', diff --git a/resources/lang/zh_CN/entities.php b/resources/lang/zh_CN/entities.php index e13a5ddf7..cce5a2ecf 100644 --- a/resources/lang/zh_CN/entities.php +++ b/resources/lang/zh_CN/entities.php @@ -254,20 +254,20 @@ return [ 'tag' => '标签', 'tags' => '标签', 'tag_name' => '标签名称', - 'tag_value' => '标签值 (Optional)', - 'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更深入的组织。", + 'tag_value' => '标签值 (可选)', + 'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更好的进行管理。", 'tags_add' => '添加另一个标签', 'tags_remove' => '删除此标签', - 'tags_usages' => 'Total tag usages', - 'tags_assigned_pages' => 'Assigned to Pages', - 'tags_assigned_chapters' => 'Assigned to Chapters', - 'tags_assigned_books' => 'Assigned to Books', - 'tags_assigned_shelves' => 'Assigned to Shelves', - 'tags_x_unique_values' => ':count unique values', - 'tags_all_values' => 'All values', - 'tags_view_tags' => 'View Tags', - 'tags_view_existing_tags' => 'View existing tags', - 'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.', + 'tags_usages' => '标签总使用量', + 'tags_assigned_pages' => '有这个标签的页面', + 'tags_assigned_chapters' => '有这个标签的章节', + 'tags_assigned_books' => '有这个标签的图书', + 'tags_assigned_shelves' => '有这个标签的书架', + 'tags_x_unique_values' => '个不重复项目', + 'tags_all_values' => '所有值', + 'tags_view_tags' => '查看标签', + 'tags_view_existing_tags' => '查看已有的标签', + 'tags_list_empty_hint' => '您可以在页面编辑器的侧边栏添加标签,或者在编辑图书、章节、书架时添加。', 'attachments' => '附件', 'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。', 'attachments_explain_instant_save' => '这里的更改将立即保存。', diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php index b662bb755..b3a28a73a 100755 --- a/resources/lang/zh_CN/settings.php +++ b/resources/lang/zh_CN/settings.php @@ -31,7 +31,7 @@ return [ 'app_custom_html_desc' => '此处添加的任何内容都将插入到每个页面的部分的底部,这对于覆盖样式或添加分析代码很方便。', 'app_custom_html_disabled_notice' => '在此设置页面上禁用了自定义HTML标题内容,以确保可以恢复所有重大更改。', 'app_logo' => '站点Logo', - 'app_logo_desc' => '这个图片的高度应该为43px。
大图片将会被缩小。', + 'app_logo_desc' => '这个图片的高度应为 43 像素。
大图片将会被缩小。', 'app_primary_color' => '站点主色', 'app_primary_color_desc' => '这应该是一个十六进制值。
保留为空以重置为默认颜色。', 'app_homepage' => '站点主页', @@ -193,7 +193,7 @@ return [ 'users_edit_profile' => '编辑资料', 'users_edit_success' => '用户更新成功', 'users_avatar' => '用户头像', - 'users_avatar_desc' => '当前图片应该为约256px的正方形。', + 'users_avatar_desc' => '选择一张头像。 这张图片应该是约 256 像素的正方形。', 'users_preferred_language' => '语言', 'users_preferred_language_desc' => '此选项将更改用于应用程序用户界面的语言。 这不会影响任何用户创建的内容。', 'users_social_accounts' => '社交账户', From cd7788f2e9e8187c290dacbacbb38608d9a50a56 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 23 Nov 2021 20:41:12 +0000 Subject: [PATCH 14/14] Updated translators and merged styleci fixes --- .github/translators.txt | 1 + app/Entities/Models/Chapter.php | 1 + app/Theming/ThemeService.php | 1 - tests/Unit/FrameworkAssumptionTest.php | 1 - 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/translators.txt b/.github/translators.txt index 57a8714f4..8d9601e72 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -199,3 +199,4 @@ M Nafis Al Mukhdi (mnafisalmukhdi1) :: Indonesian sulfo :: Danish Raukze :: German zygimantus :: Lithuanian +marinkaberg :: Russian diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index 8fc2d333d..08d6608a9 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -23,6 +23,7 @@ class Chapter extends BookChild /** * Get the pages that this chapter contains. + * * @return HasMany */ public function pages(string $dir = 'ASC'): HasMany diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index 57336ec5f..275dc9d8c 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -5,7 +5,6 @@ namespace BookStack\Theming; use BookStack\Auth\Access\SocialAuthService; use Illuminate\Console\Application; use Illuminate\Console\Application as Artisan; -use Illuminate\Contracts\Console\Kernel; use Symfony\Component\Console\Command\Command; class ThemeService diff --git a/tests/Unit/FrameworkAssumptionTest.php b/tests/Unit/FrameworkAssumptionTest.php index 8ad45612c..54d315de9 100644 --- a/tests/Unit/FrameworkAssumptionTest.php +++ b/tests/Unit/FrameworkAssumptionTest.php @@ -13,7 +13,6 @@ use Tests\TestCase; */ class FrameworkAssumptionTest extends TestCase { - public function test_scopes_error_if_not_existing() { $this->expectException(BadMethodCallException::class);