Compare commits

..

17 Commits

Author SHA1 Message Date
Dan Brown
b8354b974b Updated version and assets for release v0.28.2 2020-02-15 22:36:08 +00:00
Dan Brown
034c1e289d Merge branch 'master' into release 2020-02-15 22:35:46 +00:00
Dan Brown
01b95d91ba Fixed side-effect in binary LDAP handling
- Was not stripping prefix when sending value to LDAP server in search.
- Updated test to cover.
2020-02-15 22:35:15 +00:00
Dan Brown
f31605a3de Updated version and assets for release v0.28.1 2020-02-15 22:08:06 +00:00
Dan Brown
e7cc75c74d Merge branch 'master' into release 2020-02-15 22:07:17 +00:00
Dan Brown
54a4c6e678 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.
2020-02-15 21:37:41 +00:00
Dan Brown
29cc35a304 Added dump_user_details option to LDAP and added binary attribute decode option
Related to #1872
2020-02-15 20:31:23 +00:00
Dan Brown
6caedc7a37 Fixed issues preventing breadcrumb navigation menus from opening
- Added tests to cover endpoint

Fixes #1884
2020-02-15 19:09:33 +00:00
Dan Brown
5978d9a0d3 Updated cover image methods so image parameter is not optional but still nullable 2020-02-15 18:38:36 +00:00
Dan Brown
98ab3c1ffb Merge branch 'new_bookshelf_cover_fix' of git://github.com/TBK/BookStack into TBK-new_bookshelf_cover_fix 2020-02-15 18:34:45 +00:00
Dan Brown
ea3c3cde5a Added test to ensure shelf cover image gets set on create
Related to #1897
2020-02-15 18:34:02 +00:00
Dan Brown
e9d879bcc5 Made some updates to project readme and license 2020-02-15 15:47:17 +00:00
Dan Brown
ccd50fe918 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
2020-02-15 15:34:06 +00:00
Dan Brown
14363edb73 Fixed LDAP error thrown by not found user details
- Added testing to cover.

Related to #1876
2020-02-15 14:44:36 +00:00
Dan Brown
e8cfb4f2be Removed unintended extra lines in code blocks
Fixes #1877
2020-02-15 14:24:55 +00:00
Dan Brown
49386b42da Updated email test send to show error on failure
- Added test to cover
- Closes #1874
2020-02-15 14:13:15 +00:00
TBK
9533e0646e Fix for missing cover on create new shelf 2020-02-14 20:33:07 +01:00
32 changed files with 376 additions and 108 deletions

View File

@@ -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/

View File

@@ -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

View File

@@ -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;
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use ErrorException;
@@ -44,6 +45,13 @@ class LdapService extends ExternalAuthService
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// Clean attributes
foreach ($attributes as $index => $attribute) {
if (strpos($attribute, 'BIN;') === 0) {
$attributes[$index] = substr($attribute, strlen('BIN;'));
}
}
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
@@ -76,35 +84,56 @@ class LdapService extends ExternalAuthService
}
$userCn = $this->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;
}
/**
* 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;
}

View File

@@ -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),

View File

@@ -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');
}
/**

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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

View File

@@ -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();
}

8
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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.*

View File

@@ -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);
}

View File

@@ -111,7 +111,7 @@ function highlightWithin(parent) {
function highlightElem(elem) {
const innerCodeElem = elem.querySelector('code[class^=language-]');
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
const content = elem.textContent;
const content = elem.textContent.trimEnd();
let mode = '';
if (innerCodeElem !== null) {

View File

@@ -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:',
];

View File

@@ -238,7 +238,6 @@ code {
padding: 1px 3px;
white-space:pre-wrap;
line-height: 1.2em;
margin-bottom: 1.2em;
}
span.code {

View File

@@ -5,7 +5,6 @@
@import "text";
@import "layout";
@import "blocks";
@import "forms";
@import "tables";
@import "header";
@import "lists";

View File

@@ -4,10 +4,9 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $book->name }}</title>
@include('partials.export-styles', ['format' => $format])
<style>
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
@endif
.page-break {
page-break-after: always;
}

View File

@@ -4,10 +4,9 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $chapter->name }}</title>
@include('partials.export-styles', ['format' => $format])
<style>
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
@endif
.page-break {
page-break-after: always;
}
@@ -20,7 +19,6 @@
}
}
</style>
@yield('head')
@include('partials.custom-head')
</head>
<body>

View File

@@ -4,12 +4,31 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $page->name }}</title>
<style>
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
@endif
</style>
@yield('head')
@include('partials.export-styles', ['format' => $format])
@if($format === 'pdf')
<style>
body {
font-size: 14px;
line-height: 1.2;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
table {
max-width: 800px !important;
font-size: 0.8em;
width: 100% !important;
}
table td {
width: auto !important;
}
</style>
@endif
@include('partials.custom-head')
</head>
<body>

View File

@@ -1,34 +0,0 @@
@extends('pages/export')
@section('head')
<style>
body {
font-size: 14px;
line-height: 1.2;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
table {
max-width: 800px !important;
font-size: 0.8em;
width: 100% !important;
}
table td {
width: auto !important;
}
.page-content .float {
float: none !important;
}
.page-content img.align-left, .page-content img.align-right {
float: none !important;
clear: both;
display: block;
}
</style>
@stop

View File

@@ -0,0 +1,29 @@
<style>
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
@endif
</style>
@if ($format === 'pdf')
<style>
/* Patches for CSS variable colors */
a {
color: {{ setting('app-color') }};
}
blockquote {
border-left-color: {{ setting('app-color') }};
}
/* Patches for content layout */
.page-content .float {
float: none !important;
}
.page-content img.align-left, .page-content img.align-right {
float: none !important;
clear: both;
display: block;
}
</style>
@endif

View File

@@ -1,5 +1,6 @@
<?php namespace Tests;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\Role;
use BookStack\Auth\Access\Ldap;
use BookStack\Auth\User;
@@ -20,7 +21,7 @@ class LdapTest extends BrowserKitTest
{
parent::setUp();
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
app('config')->set([
config()->set([
'auth.method' => 'ldap',
'auth.defaults.guard' => 'ldap',
'services.ldap.base_dn' => 'dc=ldap,dc=local',
@@ -166,7 +167,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 +187,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')
@@ -543,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'), ['cn', 'dn', 'uid', 'mail', 'cn'])
->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']);
}
}

View File

@@ -1,14 +1,17 @@
<?php namespace Tests;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Uploads\Image;
use Illuminate\Support\Str;
use Tests\Uploads\UsesImages;
class BookShelfTest extends TestCase
{
use UsesImages;
public function test_shelves_shows_in_header_if_have_view_permissions()
{
$viewer = $this->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->id,
'image_id' => $lastImage->id,
]);
$this->assertEquals($lastImage->id, $shelf->cover->id);
}
public function test_shelf_view()
{
$shelf = Bookshelf::first();

View File

@@ -1,6 +1,7 @@
<?php namespace Tests;
use BookStack\Actions\Tag;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
@@ -10,7 +11,7 @@ class EntitySearchTest extends TestCase
public function test_page_search()
{
$book = \BookStack\Entities\Book::all()->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);
}
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace Tests;
use BookStack\Notifications\TestEmail;
use Illuminate\Contracts\Notifications\Dispatcher;
use Illuminate\Support\Facades\Notification;
class TestEmailTest extends TestCase
@@ -26,6 +27,24 @@ class TestEmailTest extends TestCase
Notification::assertSentTo($admin, TestEmail::class);
}
public function test_send_test_email_failure_displays_error_notification()
{
$mockDispatcher = $this->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();

View File

@@ -1 +1 @@
v0.28.0
v0.28.2