From 9533e0646ed25c3469fe7c8186be8f50e53df218 Mon Sep 17 00:00:00 2001 From: TBK Date: Fri, 14 Feb 2020 20:33:07 +0100 Subject: [PATCH 01/11] Fix for missing cover on create new shelf --- app/Http/Controllers/BookshelfController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index 57e67dc00..c882ca7c3 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -90,7 +90,7 @@ class BookshelfController extends Controller $bookIds = explode(',', $request->get('books', '')); $shelf = $this->bookshelfRepo->create($request->all(), $bookIds); - $this->bookshelfRepo->updateCoverImage($shelf); + $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null)); Activity::add($shelf, 'bookshelf_create'); return redirect($shelf->getUrl()); From 49386b42dae9ccd3d446d1819141ddd56676e3f7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 14:13:15 +0000 Subject: [PATCH 02/11] Updated email test send to show error on failure - Added test to cover - Closes #1874 --- app/Http/Controllers/SettingController.php | 10 ++++++++-- resources/lang/en/errors.php | 3 +++ tests/TestEmailTest.php | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 892b2d9cf..00dd60ac7 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -122,8 +122,14 @@ class SettingController extends Controller { $this->checkPermission('settings-manage'); - user()->notify(new TestEmail()); - $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email])); + try { + user()->notify(new TestEmail()); + $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email])); + } catch (\Exception $exception) { + $errorMessage = trans('errors.maintenance_test_email_failure') . "\n" . $exception->getMessage(); + $this->showErrorNotification($errorMessage); + } + return redirect('/settings/maintenance#image-cleanup')->withInput(); } diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index 4752d8b0c..38f1ce28a 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -96,4 +96,7 @@ return [ 'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls', 'api_user_token_expired' => 'The authorization token used has expired', + // Settings & Maintenance + 'maintenance_test_email_failure' => 'Error thrown when sending a test email:', + ]; diff --git a/tests/TestEmailTest.php b/tests/TestEmailTest.php index c06311d84..76ff322ff 100644 --- a/tests/TestEmailTest.php +++ b/tests/TestEmailTest.php @@ -1,6 +1,7 @@ mock(Dispatcher::class); + $this->app[Dispatcher::class] = $mockDispatcher; + + $exception = new \Exception('A random error occurred when testing an email'); + $mockDispatcher->shouldReceive('send')->andThrow($exception); + + $admin = $this->getAdmin(); + $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email'); + $sendReq->assertRedirect('/settings/maintenance#image-cleanup'); + $this->assertSessionHas('error'); + + $message = session()->get('error'); + $this->assertStringContainsString('Error thrown when sending a test email:', $message); + $this->assertStringContainsString('A random error occurred when testing an email', $message); + } + public function test_send_test_email_requires_settings_manage_permission() { Notification::fake(); From e8cfb4f2bed8c786da9ceccbe1599c33e23a6565 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 14:24:55 +0000 Subject: [PATCH 03/11] Removed unintended extra lines in code blocks Fixes #1877 --- resources/js/services/code.js | 2 +- resources/sass/_text.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/js/services/code.js b/resources/js/services/code.js index 718ef5721..a8cede5f4 100644 --- a/resources/js/services/code.js +++ b/resources/js/services/code.js @@ -111,7 +111,7 @@ function highlightWithin(parent) { function highlightElem(elem) { const innerCodeElem = elem.querySelector('code[class^=language-]'); elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); - const content = elem.textContent; + const content = elem.textContent.trimEnd(); let mode = ''; if (innerCodeElem !== null) { diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss index 77e0773eb..d28706781 100644 --- a/resources/sass/_text.scss +++ b/resources/sass/_text.scss @@ -238,7 +238,6 @@ code { padding: 1px 3px; white-space:pre-wrap; line-height: 1.2em; - margin-bottom: 1.2em; } span.code { From 14363edb73d8baacd5325c572b1f401f96404f58 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 14:44:36 +0000 Subject: [PATCH 04/11] Fixed LDAP error thrown by not found user details - Added testing to cover. Related to #1876 --- app/Auth/Access/Guards/LdapSessionGuard.php | 23 ++++++++++++++------- app/Auth/Access/LdapService.php | 4 ++-- tests/Auth/LdapTest.php | 19 ++++++++++++++++- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/Auth/Access/Guards/LdapSessionGuard.php b/app/Auth/Access/Guards/LdapSessionGuard.php index 3c98140f6..84f54ad29 100644 --- a/app/Auth/Access/Guards/LdapSessionGuard.php +++ b/app/Auth/Access/Guards/LdapSessionGuard.php @@ -44,11 +44,14 @@ class LdapSessionGuard extends ExternalBaseSessionGuard public function validate(array $credentials = []) { $userDetails = $this->ldapService->getUserDetails($credentials['username']); - $this->lastAttempted = $this->provider->retrieveByCredentials([ - 'external_auth_id' => $userDetails['uid'] - ]); - return $this->ldapService->validateUserCredentials($userDetails, $credentials['username'], $credentials['password']); + if (isset($userDetails['uid'])) { + $this->lastAttempted = $this->provider->retrieveByCredentials([ + 'external_auth_id' => $userDetails['uid'] + ]); + } + + return $this->ldapService->validateUserCredentials($userDetails, $credentials['password']); } /** @@ -66,11 +69,15 @@ class LdapSessionGuard extends ExternalBaseSessionGuard { $username = $credentials['username']; $userDetails = $this->ldapService->getUserDetails($username); - $this->lastAttempted = $user = $this->provider->retrieveByCredentials([ - 'external_auth_id' => $userDetails['uid'] - ]); - if (!$this->ldapService->validateUserCredentials($userDetails, $username, $credentials['password'])) { + $user = null; + if (isset($userDetails['uid'])) { + $this->lastAttempted = $user = $this->provider->retrieveByCredentials([ + 'external_auth_id' => $userDetails['uid'] + ]); + } + + if (!$this->ldapService->validateUserCredentials($userDetails, $credentials['password'])) { return false; } diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index 07e9f7b64..0d6ebfc80 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -102,9 +102,9 @@ class LdapService extends ExternalAuthService * Check if the given credentials are valid for the given user. * @throws LdapException */ - public function validateUserCredentials(array $ldapUserDetails, string $username, string $password): bool + public function validateUserCredentials(?array $ldapUserDetails, string $password): bool { - if ($ldapUserDetails === null) { + if (is_null($ldapUserDetails)) { return false; } diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index cb1194e22..06f88c222 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -166,7 +166,7 @@ class LdapTest extends BrowserKitTest ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']); } - public function test_initial_incorrect_details() + public function test_initial_incorrect_credentials() { $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); $this->mockLdap->shouldReceive('setVersion')->once(); @@ -186,6 +186,23 @@ class LdapTest extends BrowserKitTest ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]); } + public function test_login_not_found_username() + { + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); + $this->mockLdap->shouldReceive('setVersion')->once(); + $this->mockLdap->shouldReceive('setOption')->times(1); + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) + ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) + ->andReturn(['count' => 0]); + $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true, false); + $this->mockEscapes(1); + + $this->mockUserLogin() + ->seePageIs('/login')->see('These credentials do not match our records.') + ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]); + } + + public function test_create_user_form() { $this->asAdmin()->visit('/settings/users/create') From ccd50fe918518b9b9fc76d57e6b56a445ce8f014 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 15:34:06 +0000 Subject: [PATCH 05/11] Aligned export styles a little better and fixed potential DOMPDF css error - Removed different PDF template used on pages. - Updated export view files to have the intended format passed. - Shared the export CSS amoung the export templates. Should hopefully address #1886 --- app/Entities/ExportService.php | 30 +++++++++------- resources/sass/export-styles.scss | 1 - resources/views/books/export.blade.php | 5 ++- resources/views/chapters/export.blade.php | 6 ++-- resources/views/pages/export.blade.php | 31 +++++++++++++---- resources/views/pages/pdf.blade.php | 34 ------------------- .../views/partials/export-styles.blade.php | 29 ++++++++++++++++ 7 files changed, 76 insertions(+), 60 deletions(-) delete mode 100644 resources/views/pages/pdf.blade.php create mode 100644 resources/views/partials/export-styles.blade.php diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php index 3ec867959..f945dfbe4 100644 --- a/app/Entities/ExportService.php +++ b/app/Entities/ExportService.php @@ -29,8 +29,9 @@ class ExportService public function pageToContainedHtml(Page $page) { $page->html = (new PageContent($page))->render(); - $pageHtml = view('pages/export', [ - 'page' => $page + $pageHtml = view('pages.export', [ + 'page' => $page, + 'format' => 'html', ])->render(); return $this->containHtml($pageHtml); } @@ -45,9 +46,10 @@ class ExportService $pages->each(function ($page) { $page->html = (new PageContent($page))->render(); }); - $html = view('chapters/export', [ + $html = view('chapters.export', [ 'chapter' => $chapter, - 'pages' => $pages + 'pages' => $pages, + 'format' => 'html', ])->render(); return $this->containHtml($html); } @@ -59,9 +61,10 @@ class ExportService public function bookToContainedHtml(Book $book) { $bookTree = (new BookContents($book))->getTree(false, true); - $html = view('books/export', [ + $html = view('books.export', [ 'book' => $book, - 'bookChildren' => $bookTree + 'bookChildren' => $bookTree, + 'format' => 'html', ])->render(); return $this->containHtml($html); } @@ -73,8 +76,9 @@ class ExportService public function pageToPdf(Page $page) { $page->html = (new PageContent($page))->render(); - $html = view('pages/pdf', [ - 'page' => $page + $html = view('pages.export', [ + 'page' => $page, + 'format' => 'pdf', ])->render(); return $this->htmlToPdf($html); } @@ -90,9 +94,10 @@ class ExportService $page->html = (new PageContent($page))->render(); }); - $html = view('chapters/export', [ + $html = view('chapters.export', [ 'chapter' => $chapter, - 'pages' => $pages + 'pages' => $pages, + 'format' => 'pdf', ])->render(); return $this->htmlToPdf($html); @@ -105,9 +110,10 @@ class ExportService public function bookToPdf(Book $book) { $bookTree = (new BookContents($book))->getTree(false, true); - $html = view('books/export', [ + $html = view('books.export', [ 'book' => $book, - 'bookChildren' => $bookTree + 'bookChildren' => $bookTree, + 'format' => 'pdf', ])->render(); return $this->htmlToPdf($html); } diff --git a/resources/sass/export-styles.scss b/resources/sass/export-styles.scss index 958b78807..6d9a1a718 100644 --- a/resources/sass/export-styles.scss +++ b/resources/sass/export-styles.scss @@ -5,7 +5,6 @@ @import "text"; @import "layout"; @import "blocks"; -@import "forms"; @import "tables"; @import "header"; @import "lists"; diff --git a/resources/views/books/export.blade.php b/resources/views/books/export.blade.php index 1cf91046d..e86a24e81 100644 --- a/resources/views/books/export.blade.php +++ b/resources/views/books/export.blade.php @@ -4,10 +4,9 @@ {{ $book->name }} + @include('partials.export-styles', ['format' => $format]) + - @yield('head') @include('partials.custom-head') diff --git a/resources/views/pages/export.blade.php b/resources/views/pages/export.blade.php index 4746a56f3..47a4d870a 100644 --- a/resources/views/pages/export.blade.php +++ b/resources/views/pages/export.blade.php @@ -4,12 +4,31 @@ {{ $page->name }} - - @yield('head') + @include('partials.export-styles', ['format' => $format]) + + @if($format === 'pdf') + + @endif + @include('partials.custom-head') diff --git a/resources/views/pages/pdf.blade.php b/resources/views/pages/pdf.blade.php deleted file mode 100644 index 33a009fee..000000000 --- a/resources/views/pages/pdf.blade.php +++ /dev/null @@ -1,34 +0,0 @@ -@extends('pages/export') - -@section('head') - -@stop \ No newline at end of file diff --git a/resources/views/partials/export-styles.blade.php b/resources/views/partials/export-styles.blade.php new file mode 100644 index 000000000..52bfda2a6 --- /dev/null +++ b/resources/views/partials/export-styles.blade.php @@ -0,0 +1,29 @@ + + +@if ($format === 'pdf') + +@endif \ No newline at end of file From e9d879bcc556a46d99fd63387b5893abda26a034 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 15:47:17 +0000 Subject: [PATCH 06/11] Made some updates to project readme and license --- LICENSE | 2 +- readme.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 080c54b3e..61aeaad8c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Dan Brown and the BookStack Project contributors +Copyright (c) 2020 Dan Brown and the BookStack Project contributors https://github.com/BookStackApp/BookStack/graphs/contributors Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/readme.md b/readme.md index fb26cede3..5b51b8eab 100644 --- a/readme.md +++ b/readme.md @@ -2,10 +2,11 @@ [![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE) +[![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack) [![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions) [![Discord](https://img.shields.io/static/v1?label=Chat&message=Discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2) -A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. +A platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/. * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation) * [Documentation](https://www.bookstackapp.com/docs) @@ -25,7 +26,7 @@ In regards to development philosophy, BookStack has a relaxed, open & positive a Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below. -- **Platform REST API** *(In Design)* +- **Platform REST API** *(Base Implemented, In review and roll-out)* - *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.* - **Editor Alignment & Review** - *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.* From ea3c3cde5aa7e25dd68821fe4b1b8247b2ab714a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 18:34:02 +0000 Subject: [PATCH 07/11] Added test to ensure shelf cover image gets set on create Related to #1897 --- tests/Entity/BookShelfTest.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 5c7673847..b44761203 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -1,14 +1,17 @@ getViewer(); @@ -83,6 +86,26 @@ class BookShelfTest extends TestCase $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]); } + public function test_shelves_create_sets_cover_image() + { + $shelfInfo = [ + 'name' => 'My test book' . Str::random(4), + 'description' => 'Test book description ' . Str::random(10) + ]; + + $imageFile = $this->getTestImage('shelf-test.png'); + $resp = $this->asEditor()->call('POST', '/shelves', $shelfInfo, [], ['image' => $imageFile]); + $resp->assertRedirect(); + + $lastImage = Image::query()->orderByDesc('id')->firstOrFail(); + $shelf = Bookshelf::query()->where('name', '=', $shelfInfo['name'])->first(); + $this->assertDatabaseHas('bookshelves', [ + 'id' => $shelf, + 'image_id' => $lastImage->id, + ]); + $this->assertEquals($lastImage->id, $shelf->cover->id); + } + public function test_shelf_view() { $shelf = Bookshelf::first(); From 5978d9a0d3766a884aa46d5fc2085eb3420a8ba8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 18:38:36 +0000 Subject: [PATCH 08/11] Updated cover image methods so image parameter is not optional but still nullable --- app/Entities/Repos/BaseRepo.php | 2 +- app/Entities/Repos/BookRepo.php | 2 +- app/Entities/Repos/BookshelfRepo.php | 2 +- tests/Entity/BookShelfTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 23f07f820..7c25e4981 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -76,7 +76,7 @@ class BaseRepo * @throws ImageUploadException * @throws \Exception */ - public function updateCoverImage(HasCoverImage $entity, UploadedFile $coverImage = null, bool $removeImage = false) + public function updateCoverImage(HasCoverImage $entity, ?UploadedFile $coverImage, bool $removeImage = false) { if ($coverImage) { $this->imageRepo->destroyImage($entity->cover); diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 7fcc80fac..70db0fa65 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -108,7 +108,7 @@ class BookRepo * @throws ImageUploadException * @throws Exception */ - public function updateCoverImage(Book $book, UploadedFile $coverImage = null, bool $removeImage = false) + public function updateCoverImage(Book $book, ?UploadedFile $coverImage, bool $removeImage = false) { $this->baseRepo->updateCoverImage($book, $coverImage, $removeImage); } diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index 03b54f009..25fa97dae 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -123,7 +123,7 @@ class BookshelfRepo * @throws ImageUploadException * @throws Exception */ - public function updateCoverImage(Bookshelf $shelf, UploadedFile $coverImage = null, bool $removeImage = false) + public function updateCoverImage(Bookshelf $shelf, ?UploadedFile $coverImage, bool $removeImage = false) { $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage); } diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index b44761203..a318ebe24 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -100,7 +100,7 @@ class BookShelfTest extends TestCase $lastImage = Image::query()->orderByDesc('id')->firstOrFail(); $shelf = Bookshelf::query()->where('name', '=', $shelfInfo['name'])->first(); $this->assertDatabaseHas('bookshelves', [ - 'id' => $shelf, + 'id' => $shelf->id, 'image_id' => $lastImage->id, ]); $this->assertEquals($lastImage->id, $shelf->cover->id); From 6caedc7a37a5ca6cee39b69c3c651230a5e30b52 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 19:09:33 +0000 Subject: [PATCH 09/11] Fixed issues preventing breadcrumb navigation menus from opening - Added tests to cover endpoint Fixes #1884 --- app/Entities/Book.php | 2 +- app/Http/Controllers/SearchController.php | 2 +- tests/Entity/EntitySearchTest.php | 84 +++++++++++++++++++++-- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/app/Entities/Book.php b/app/Entities/Book.php index 919f60035..df0d99228 100644 --- a/app/Entities/Book.php +++ b/app/Entities/Book.php @@ -115,7 +115,7 @@ class Book extends Entity implements HasCoverImage { $pages = $this->directPages()->visible()->get(); $chapters = $this->chapters()->visible()->get(); - return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft'); + return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft'); } /** diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index a5cd7ad6b..a5d57741d 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -109,7 +109,7 @@ class SearchController extends Controller // Page in chapter if ($entity->isA('page') && $entity->chapter) { - $entities = $entity->chapter->visiblePages(); + $entities = $entity->chapter->getVisiblePages(); } // Page in book or chapter diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 3eb50a412..34c3cd4a8 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -1,6 +1,7 @@ first(); + $book = Book::all()->first(); $page = $book->pages->first(); $search = $this->asEditor()->get('/search?term=' . urlencode($page->name)); @@ -54,7 +55,7 @@ class EntitySearchTest extends TestCase public function test_book_search() { - $book = \BookStack\Entities\Book::first(); + $book = Book::first(); $page = $book->pages->last(); $chapter = $book->chapters->last(); @@ -67,7 +68,7 @@ class EntitySearchTest extends TestCase public function test_chapter_search() { - $chapter = \BookStack\Entities\Chapter::has('pages')->first(); + $chapter = Chapter::has('pages')->first(); $page = $chapter->pages[0]; $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name)); @@ -77,11 +78,11 @@ class EntitySearchTest extends TestCase public function test_tag_search() { $newTags = [ - new \BookStack\Actions\Tag([ + new Tag([ 'name' => 'animal', 'value' => 'cat' ]), - new \BookStack\Actions\Tag([ + new Tag([ 'name' => 'color', 'value' => 'red' ]) @@ -204,4 +205,75 @@ class EntitySearchTest extends TestCase $chapterSearch->assertSee($chapter->name); $chapterSearch->assertSee($chapter->book->getShortName(42)); } + + public function test_sibling_search_for_pages() + { + $chapter = Chapter::query()->with('pages')->first(); + $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling'); + $page = $chapter->pages->first(); + + $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); + $search->assertSuccessful(); + foreach ($chapter->pages as $page) { + $search->assertSee($page->name); + } + + $search->assertDontSee($chapter->name); + } + + public function test_sibling_search_for_pages_without_chapter() + { + $page = Page::query()->where('chapter_id', '=', 0)->firstOrFail(); + $bookChildren = $page->book->getDirectChildren(); + $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); + $search->assertSuccessful(); + foreach ($bookChildren as $child) { + $search->assertSee($child->name); + } + + $search->assertDontSee($page->book->name); + } + + public function test_sibling_search_for_chapters() + { + $chapter = Chapter::query()->firstOrFail(); + $bookChildren = $chapter->book->getDirectChildren(); + $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter"); + $search->assertSuccessful(); + foreach ($bookChildren as $child) { + $search->assertSee($child->name); + } + + $search->assertDontSee($chapter->book->name); + } + + public function test_sibling_search_for_books() + { + $books = Book::query()->take(10)->get(); + $book = $books->first(); + $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book"); + $search->assertSuccessful(); + foreach ($books as $expectedBook) { + $search->assertSee($expectedBook->name); + } + } + + public function test_sibling_search_for_shelves() + { + $shelves = Bookshelf::query()->take(10)->get(); + $shelf = $shelves->first(); + $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling'); + + $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf"); + $search->assertSuccessful(); + foreach ($shelves as $expectedShelf) { + $search->assertSee($expectedShelf->name); + } + } } From 29cc35a304e4ca19db6bad77087fdf1b8a3f8ac5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 20:31:23 +0000 Subject: [PATCH 10/11] Added dump_user_details option to LDAP and added binary attribute decode option Related to #1872 --- .env.example.complete | 1 + app/Auth/Access/LdapService.php | 30 ++++++++++++++++--- app/Config/services.php | 1 + tests/Auth/LdapTest.php | 52 ++++++++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/.env.example.complete b/.env.example.complete index 04cd73b90..86a7351c2 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -200,6 +200,7 @@ LDAP_ID_ATTRIBUTE=uid LDAP_EMAIL_ATTRIBUTE=mail LDAP_DISPLAY_NAME_ATTRIBUTE=cn LDAP_FOLLOW_REFERRALS=true +LDAP_DUMP_USER_DETAILS=false # LDAP group sync configuration # Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/ diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index 0d6ebfc80..d37770558 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -1,6 +1,7 @@ getUserResponseProperty($user, 'cn', null); - return [ + $formatted = [ 'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']), 'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn), 'dn' => $user['dn'], 'email' => $this->getUserResponseProperty($user, $emailAttr, null), ]; + + if ($this->config['dump_user_details']) { + throw new JsonDebugException([ + 'details_from_ldap' => $user, + 'details_bookstack_parsed' => $formatted, + ]); + } + + return $formatted; } /** * Get a property from an LDAP user response fetch. * Handles properties potentially being part of an array. + * If the given key is prefixed with 'BIN;', that indicator will be stripped + * from the key and any fetched values will be converted from binary to hex. */ protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue) { + $isBinary = strpos($propertyKey, 'BIN;') === 0; $propertyKey = strtolower($propertyKey); - if (isset($userDetails[$propertyKey])) { - return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]); + $value = $defaultValue; + + if ($isBinary) { + $propertyKey = substr($propertyKey, strlen('BIN;')); } - return $defaultValue; + if (isset($userDetails[$propertyKey])) { + $value = (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]); + if ($isBinary) { + $value = bin2hex($value); + } + } + + return $value; } /** diff --git a/app/Config/services.php b/app/Config/services.php index a0bdd078a..fcde621d2 100644 --- a/app/Config/services.php +++ b/app/Config/services.php @@ -118,6 +118,7 @@ return [ 'ldap' => [ 'server' => env('LDAP_SERVER', false), + 'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false), 'dn' => env('LDAP_DN', false), 'pass' => env('LDAP_PASS', false), 'base_dn' => env('LDAP_BASE_DN', false), diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 06f88c222..f6c5997b3 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -1,5 +1,6 @@ set([ + config()->set([ 'auth.method' => 'ldap', 'auth.defaults.guard' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', @@ -560,4 +561,53 @@ class LdapTest extends BrowserKitTest $resp = $this->post('/register'); $this->assertPermissionError($resp); } + + public function test_dump_user_details_option_works() + { + config()->set(['services.ldap.dump_user_details' => true]); + + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); + $this->mockLdap->shouldReceive('setVersion')->once(); + $this->mockLdap->shouldReceive('setOption')->times(1); + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) + ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) + ->andReturn(['count' => 1, 0 => [ + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')] + ]]); + $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true); + $this->mockEscapes(1); + + $this->post('/login', [ + 'username' => $this->mockUser->name, + 'password' => $this->mockUser->password, + ]); + $this->seeJsonStructure([ + 'details_from_ldap' => [], + 'details_bookstack_parsed' => [], + ]); + } + + public function test_ldap_attributes_can_be_binary_decoded_if_marked() + { + config()->set(['services.ldap.id_attribute' => 'BIN;uid']); + $ldapService = app()->make(LdapService::class); + + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); + $this->mockLdap->shouldReceive('setVersion')->once(); + $this->mockLdap->shouldReceive('setOption')->times(1); + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) + ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) + ->andReturn(['count' => 1, 0 => [ + 'uid' => [hex2bin('FFF8F7')], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')] + ]]); + $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true); + $this->mockEscapes(1); + + $details = $ldapService->getUserDetails('test'); + $this->assertEquals('fff8f7', $details['uid']); + } } From 54a4c6e6787734557f71e8a21719a96e9b09fee4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 15 Feb 2020 21:37:41 +0000 Subject: [PATCH 11/11] Fixed code-block drag+drop handling - Added custom handling, Tracks if contenteditable blocks are being dragged. On drop the selection location will be roughly checked to put the block above or below the cursor block root element. --- resources/js/components/wysiwyg-editor.js | 34 +++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index b9e3340a8..7818db260 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -593,6 +593,7 @@ class WysiwygEditor { registerEditorShortcuts(editor); let wrap; + let draggedContentEditable; function hasTextContent(node) { return node && !!( node.textContent || node.innerText ); @@ -601,12 +602,19 @@ class WysiwygEditor { editor.on('dragstart', function () { let node = editor.selection.getNode(); - if (node.nodeName !== 'IMG') return; - wrap = editor.dom.getParent(node, '.mceTemp'); + if (node.nodeName === 'IMG') { + wrap = editor.dom.getParent(node, '.mceTemp'); - if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) { - wrap = node.parentNode; + if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) { + wrap = node.parentNode; + } } + + // Track dragged contenteditable blocks + if (node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') === 'false') { + draggedContentEditable = node; + } + }); editor.on('drop', function (event) { @@ -614,7 +622,7 @@ class WysiwygEditor { rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc()); // Template insertion - const templateId = event.dataTransfer.getData('bookstack/template'); + const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template'); if (templateId) { event.preventDefault(); window.$http.get(`/templates/${templateId}`).then(resp => { @@ -638,6 +646,22 @@ class WysiwygEditor { }); } + // Handle contenteditable section drop + if (!event.isDefaultPrevented() && draggedContentEditable) { + event.preventDefault(); + editor.undoManager.transact(function () { + const selectedNode = editor.selection.getNode(); + const range = editor.selection.getRng(); + const selectedNodeRoot = selectedNode.closest('body > *'); + if (range.startOffset > (range.startContainer.length / 2)) { + editor.$(selectedNodeRoot).after(draggedContentEditable); + } else { + editor.$(selectedNodeRoot).before(draggedContentEditable); + } + }); + } + + // Handle image insert if (!event.isDefaultPrevented()) { editorPaste(event, editor, context); }