Merge branch 'master' into release
This commit is contained in:
commit
e7cc75c74d
|
@ -200,6 +200,7 @@ LDAP_ID_ATTRIBUTE=uid
|
||||||
LDAP_EMAIL_ATTRIBUTE=mail
|
LDAP_EMAIL_ATTRIBUTE=mail
|
||||||
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
|
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
|
||||||
LDAP_FOLLOW_REFERRALS=true
|
LDAP_FOLLOW_REFERRALS=true
|
||||||
|
LDAP_DUMP_USER_DETAILS=false
|
||||||
|
|
||||||
# LDAP group sync configuration
|
# LDAP group sync configuration
|
||||||
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
|
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
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
|
https://github.com/BookStackApp/BookStack/graphs/contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
|
|
@ -44,11 +44,14 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
public function validate(array $credentials = [])
|
public function validate(array $credentials = [])
|
||||||
{
|
{
|
||||||
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
||||||
|
|
||||||
|
if (isset($userDetails['uid'])) {
|
||||||
$this->lastAttempted = $this->provider->retrieveByCredentials([
|
$this->lastAttempted = $this->provider->retrieveByCredentials([
|
||||||
'external_auth_id' => $userDetails['uid']
|
'external_auth_id' => $userDetails['uid']
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->ldapService->validateUserCredentials($userDetails, $credentials['username'], $credentials['password']);
|
return $this->ldapService->validateUserCredentials($userDetails, $credentials['password']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,11 +69,15 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
{
|
{
|
||||||
$username = $credentials['username'];
|
$username = $credentials['username'];
|
||||||
$userDetails = $this->ldapService->getUserDetails($username);
|
$userDetails = $this->ldapService->getUserDetails($username);
|
||||||
|
|
||||||
|
$user = null;
|
||||||
|
if (isset($userDetails['uid'])) {
|
||||||
$this->lastAttempted = $user = $this->provider->retrieveByCredentials([
|
$this->lastAttempted = $user = $this->provider->retrieveByCredentials([
|
||||||
'external_auth_id' => $userDetails['uid']
|
'external_auth_id' => $userDetails['uid']
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->ldapService->validateUserCredentials($userDetails, $username, $credentials['password'])) {
|
if (!$this->ldapService->validateUserCredentials($userDetails, $credentials['password'])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php namespace BookStack\Auth\Access;
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\LdapException;
|
use BookStack\Exceptions\LdapException;
|
||||||
use ErrorException;
|
use ErrorException;
|
||||||
|
|
||||||
|
@ -76,35 +77,56 @@ class LdapService extends ExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
$userCn = $this->getUserResponseProperty($user, 'cn', null);
|
$userCn = $this->getUserResponseProperty($user, 'cn', null);
|
||||||
return [
|
$formatted = [
|
||||||
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
||||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||||
'dn' => $user['dn'],
|
'dn' => $user['dn'],
|
||||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
'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.
|
* Get a property from an LDAP user response fetch.
|
||||||
* Handles properties potentially being part of an array.
|
* 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)
|
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
|
||||||
{
|
{
|
||||||
|
$isBinary = strpos($propertyKey, 'BIN;') === 0;
|
||||||
$propertyKey = strtolower($propertyKey);
|
$propertyKey = strtolower($propertyKey);
|
||||||
if (isset($userDetails[$propertyKey])) {
|
$value = $defaultValue;
|
||||||
return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
|
|
||||||
|
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.
|
* Check if the given credentials are valid for the given user.
|
||||||
* @throws LdapException
|
* @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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ return [
|
||||||
|
|
||||||
'ldap' => [
|
'ldap' => [
|
||||||
'server' => env('LDAP_SERVER', false),
|
'server' => env('LDAP_SERVER', false),
|
||||||
|
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
|
||||||
'dn' => env('LDAP_DN', false),
|
'dn' => env('LDAP_DN', false),
|
||||||
'pass' => env('LDAP_PASS', false),
|
'pass' => env('LDAP_PASS', false),
|
||||||
'base_dn' => env('LDAP_BASE_DN', false),
|
'base_dn' => env('LDAP_BASE_DN', false),
|
||||||
|
|
|
@ -115,7 +115,7 @@ class Book extends Entity implements HasCoverImage
|
||||||
{
|
{
|
||||||
$pages = $this->directPages()->visible()->get();
|
$pages = $this->directPages()->visible()->get();
|
||||||
$chapters = $this->chapters()->visible()->get();
|
$chapters = $this->chapters()->visible()->get();
|
||||||
return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
|
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,8 +29,9 @@ class ExportService
|
||||||
public function pageToContainedHtml(Page $page)
|
public function pageToContainedHtml(Page $page)
|
||||||
{
|
{
|
||||||
$page->html = (new PageContent($page))->render();
|
$page->html = (new PageContent($page))->render();
|
||||||
$pageHtml = view('pages/export', [
|
$pageHtml = view('pages.export', [
|
||||||
'page' => $page
|
'page' => $page,
|
||||||
|
'format' => 'html',
|
||||||
])->render();
|
])->render();
|
||||||
return $this->containHtml($pageHtml);
|
return $this->containHtml($pageHtml);
|
||||||
}
|
}
|
||||||
|
@ -45,9 +46,10 @@ class ExportService
|
||||||
$pages->each(function ($page) {
|
$pages->each(function ($page) {
|
||||||
$page->html = (new PageContent($page))->render();
|
$page->html = (new PageContent($page))->render();
|
||||||
});
|
});
|
||||||
$html = view('chapters/export', [
|
$html = view('chapters.export', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'pages' => $pages
|
'pages' => $pages,
|
||||||
|
'format' => 'html',
|
||||||
])->render();
|
])->render();
|
||||||
return $this->containHtml($html);
|
return $this->containHtml($html);
|
||||||
}
|
}
|
||||||
|
@ -59,9 +61,10 @@ class ExportService
|
||||||
public function bookToContainedHtml(Book $book)
|
public function bookToContainedHtml(Book $book)
|
||||||
{
|
{
|
||||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||||
$html = view('books/export', [
|
$html = view('books.export', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'bookChildren' => $bookTree
|
'bookChildren' => $bookTree,
|
||||||
|
'format' => 'html',
|
||||||
])->render();
|
])->render();
|
||||||
return $this->containHtml($html);
|
return $this->containHtml($html);
|
||||||
}
|
}
|
||||||
|
@ -73,8 +76,9 @@ class ExportService
|
||||||
public function pageToPdf(Page $page)
|
public function pageToPdf(Page $page)
|
||||||
{
|
{
|
||||||
$page->html = (new PageContent($page))->render();
|
$page->html = (new PageContent($page))->render();
|
||||||
$html = view('pages/pdf', [
|
$html = view('pages.export', [
|
||||||
'page' => $page
|
'page' => $page,
|
||||||
|
'format' => 'pdf',
|
||||||
])->render();
|
])->render();
|
||||||
return $this->htmlToPdf($html);
|
return $this->htmlToPdf($html);
|
||||||
}
|
}
|
||||||
|
@ -90,9 +94,10 @@ class ExportService
|
||||||
$page->html = (new PageContent($page))->render();
|
$page->html = (new PageContent($page))->render();
|
||||||
});
|
});
|
||||||
|
|
||||||
$html = view('chapters/export', [
|
$html = view('chapters.export', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'pages' => $pages
|
'pages' => $pages,
|
||||||
|
'format' => 'pdf',
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
return $this->htmlToPdf($html);
|
return $this->htmlToPdf($html);
|
||||||
|
@ -105,9 +110,10 @@ class ExportService
|
||||||
public function bookToPdf(Book $book)
|
public function bookToPdf(Book $book)
|
||||||
{
|
{
|
||||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||||
$html = view('books/export', [
|
$html = view('books.export', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'bookChildren' => $bookTree
|
'bookChildren' => $bookTree,
|
||||||
|
'format' => 'pdf',
|
||||||
])->render();
|
])->render();
|
||||||
return $this->htmlToPdf($html);
|
return $this->htmlToPdf($html);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ class BaseRepo
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws \Exception
|
* @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) {
|
if ($coverImage) {
|
||||||
$this->imageRepo->destroyImage($entity->cover);
|
$this->imageRepo->destroyImage($entity->cover);
|
||||||
|
|
|
@ -108,7 +108,7 @@ class BookRepo
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws Exception
|
* @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);
|
$this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ class BookshelfRepo
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws Exception
|
* @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);
|
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class BookshelfController extends Controller
|
||||||
|
|
||||||
$bookIds = explode(',', $request->get('books', ''));
|
$bookIds = explode(',', $request->get('books', ''));
|
||||||
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
|
||||||
$this->bookshelfRepo->updateCoverImage($shelf);
|
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
|
||||||
|
|
||||||
Activity::add($shelf, 'bookshelf_create');
|
Activity::add($shelf, 'bookshelf_create');
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
|
|
|
@ -109,7 +109,7 @@ class SearchController extends Controller
|
||||||
|
|
||||||
// Page in chapter
|
// Page in chapter
|
||||||
if ($entity->isA('page') && $entity->chapter) {
|
if ($entity->isA('page') && $entity->chapter) {
|
||||||
$entities = $entity->chapter->visiblePages();
|
$entities = $entity->chapter->getVisiblePages();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page in book or chapter
|
// Page in book or chapter
|
||||||
|
|
|
@ -122,8 +122,14 @@ class SettingController extends Controller
|
||||||
{
|
{
|
||||||
$this->checkPermission('settings-manage');
|
$this->checkPermission('settings-manage');
|
||||||
|
|
||||||
|
try {
|
||||||
user()->notify(new TestEmail());
|
user()->notify(new TestEmail());
|
||||||
$this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
|
$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();
|
return redirect('/settings/maintenance#image-cleanup')->withInput();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
[](https://github.com/BookStackApp/BookStack/releases/latest)
|
[](https://github.com/BookStackApp/BookStack/releases/latest)
|
||||||
[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
|
[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
|
||||||
|
[](https://crowdin.com/project/bookstack)
|
||||||
[](https://github.com/BookStackApp/BookStack/actions)
|
[](https://github.com/BookStackApp/BookStack/actions)
|
||||||
[](https://discord.gg/ztkBqR2)
|
[](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)
|
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
||||||
* [Documentation](https://www.bookstackapp.com/docs)
|
* [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.
|
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.*
|
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
|
||||||
- **Editor Alignment & Review**
|
- **Editor Alignment & Review**
|
||||||
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
|
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
|
||||||
|
|
|
@ -593,6 +593,7 @@ class WysiwygEditor {
|
||||||
registerEditorShortcuts(editor);
|
registerEditorShortcuts(editor);
|
||||||
|
|
||||||
let wrap;
|
let wrap;
|
||||||
|
let draggedContentEditable;
|
||||||
|
|
||||||
function hasTextContent(node) {
|
function hasTextContent(node) {
|
||||||
return node && !!( node.textContent || node.innerText );
|
return node && !!( node.textContent || node.innerText );
|
||||||
|
@ -601,12 +602,19 @@ class WysiwygEditor {
|
||||||
editor.on('dragstart', function () {
|
editor.on('dragstart', function () {
|
||||||
let node = editor.selection.getNode();
|
let node = editor.selection.getNode();
|
||||||
|
|
||||||
if (node.nodeName !== 'IMG') return;
|
if (node.nodeName === 'IMG') {
|
||||||
wrap = editor.dom.getParent(node, '.mceTemp');
|
wrap = editor.dom.getParent(node, '.mceTemp');
|
||||||
|
|
||||||
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
|
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
|
||||||
wrap = node.parentNode;
|
wrap = node.parentNode;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track dragged contenteditable blocks
|
||||||
|
if (node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') === 'false') {
|
||||||
|
draggedContentEditable = node;
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.on('drop', function (event) {
|
editor.on('drop', function (event) {
|
||||||
|
@ -614,7 +622,7 @@ class WysiwygEditor {
|
||||||
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
|
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
|
||||||
|
|
||||||
// Template insertion
|
// Template insertion
|
||||||
const templateId = event.dataTransfer.getData('bookstack/template');
|
const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.$http.get(`/templates/${templateId}`).then(resp => {
|
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()) {
|
if (!event.isDefaultPrevented()) {
|
||||||
editorPaste(event, editor, context);
|
editorPaste(event, editor, context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ function highlightWithin(parent) {
|
||||||
function highlightElem(elem) {
|
function highlightElem(elem) {
|
||||||
const innerCodeElem = elem.querySelector('code[class^=language-]');
|
const innerCodeElem = elem.querySelector('code[class^=language-]');
|
||||||
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
|
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
|
||||||
const content = elem.textContent;
|
const content = elem.textContent.trimEnd();
|
||||||
|
|
||||||
let mode = '';
|
let mode = '';
|
||||||
if (innerCodeElem !== null) {
|
if (innerCodeElem !== null) {
|
||||||
|
|
|
@ -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_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',
|
'api_user_token_expired' => 'The authorization token used has expired',
|
||||||
|
|
||||||
|
// Settings & Maintenance
|
||||||
|
'maintenance_test_email_failure' => 'Error thrown when sending a test email:',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -238,7 +238,6 @@ code {
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
white-space:pre-wrap;
|
white-space:pre-wrap;
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
margin-bottom: 1.2em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span.code {
|
span.code {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
@import "text";
|
@import "text";
|
||||||
@import "layout";
|
@import "layout";
|
||||||
@import "blocks";
|
@import "blocks";
|
||||||
@import "forms";
|
|
||||||
@import "tables";
|
@import "tables";
|
||||||
@import "header";
|
@import "header";
|
||||||
@import "lists";
|
@import "lists";
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<title>{{ $book->name }}</title>
|
<title>{{ $book->name }}</title>
|
||||||
|
|
||||||
|
@include('partials.export-styles', ['format' => $format])
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@if (!app()->environment('testing'))
|
|
||||||
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
|
|
||||||
@endif
|
|
||||||
.page-break {
|
.page-break {
|
||||||
page-break-after: always;
|
page-break-after: always;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<title>{{ $chapter->name }}</title>
|
<title>{{ $chapter->name }}</title>
|
||||||
|
|
||||||
|
@include('partials.export-styles', ['format' => $format])
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@if (!app()->environment('testing'))
|
|
||||||
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
|
|
||||||
@endif
|
|
||||||
.page-break {
|
.page-break {
|
||||||
page-break-after: always;
|
page-break-after: always;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +19,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@yield('head')
|
|
||||||
@include('partials.custom-head')
|
@include('partials.custom-head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -4,12 +4,31 @@
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<title>{{ $page->name }}</title>
|
<title>{{ $page->name }}</title>
|
||||||
|
|
||||||
|
@include('partials.export-styles', ['format' => $format])
|
||||||
|
|
||||||
|
@if($format === 'pdf')
|
||||||
<style>
|
<style>
|
||||||
@if (!app()->environment('testing'))
|
body {
|
||||||
{!! file_get_contents(public_path('/dist/export-styles.css')) !!}
|
font-size: 14px;
|
||||||
@endif
|
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>
|
</style>
|
||||||
@yield('head')
|
@endif
|
||||||
|
|
||||||
@include('partials.custom-head')
|
@include('partials.custom-head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace Tests;
|
<?php namespace Tests;
|
||||||
|
|
||||||
|
use BookStack\Auth\Access\LdapService;
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\Access\Ldap;
|
use BookStack\Auth\Access\Ldap;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
@ -20,7 +21,7 @@ class LdapTest extends BrowserKitTest
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
|
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
|
||||||
app('config')->set([
|
config()->set([
|
||||||
'auth.method' => 'ldap',
|
'auth.method' => 'ldap',
|
||||||
'auth.defaults.guard' => 'ldap',
|
'auth.defaults.guard' => 'ldap',
|
||||||
'services.ldap.base_dn' => 'dc=ldap,dc=local',
|
'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']);
|
->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('connect')->once()->andReturn($this->resourceId);
|
||||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||||
|
@ -186,6 +187,23 @@ class LdapTest extends BrowserKitTest
|
||||||
->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
|
->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()
|
public function test_create_user_form()
|
||||||
{
|
{
|
||||||
$this->asAdmin()->visit('/settings/users/create')
|
$this->asAdmin()->visit('/settings/users/create')
|
||||||
|
@ -543,4 +561,53 @@ class LdapTest extends BrowserKitTest
|
||||||
$resp = $this->post('/register');
|
$resp = $this->post('/register');
|
||||||
$this->assertPermissionError($resp);
|
$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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
<?php namespace Tests;
|
<?php namespace Tests;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Book;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Bookshelf;
|
||||||
|
use BookStack\Uploads\Image;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Tests\Uploads\UsesImages;
|
||||||
|
|
||||||
class BookShelfTest extends TestCase
|
class BookShelfTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
||||||
|
use UsesImages;
|
||||||
|
|
||||||
public function test_shelves_shows_in_header_if_have_view_permissions()
|
public function test_shelves_shows_in_header_if_have_view_permissions()
|
||||||
{
|
{
|
||||||
$viewer = $this->getViewer();
|
$viewer = $this->getViewer();
|
||||||
|
@ -83,6 +86,26 @@ class BookShelfTest extends TestCase
|
||||||
$this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);
|
$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()
|
public function test_shelf_view()
|
||||||
{
|
{
|
||||||
$shelf = Bookshelf::first();
|
$shelf = Bookshelf::first();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php namespace Tests;
|
<?php namespace Tests;
|
||||||
|
|
||||||
|
use BookStack\Actions\Tag;
|
||||||
|
use BookStack\Entities\Book;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Bookshelf;
|
||||||
use BookStack\Entities\Chapter;
|
use BookStack\Entities\Chapter;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Page;
|
||||||
|
@ -10,7 +11,7 @@ class EntitySearchTest extends TestCase
|
||||||
|
|
||||||
public function test_page_search()
|
public function test_page_search()
|
||||||
{
|
{
|
||||||
$book = \BookStack\Entities\Book::all()->first();
|
$book = Book::all()->first();
|
||||||
$page = $book->pages->first();
|
$page = $book->pages->first();
|
||||||
|
|
||||||
$search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
|
$search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
|
||||||
|
@ -54,7 +55,7 @@ class EntitySearchTest extends TestCase
|
||||||
|
|
||||||
public function test_book_search()
|
public function test_book_search()
|
||||||
{
|
{
|
||||||
$book = \BookStack\Entities\Book::first();
|
$book = Book::first();
|
||||||
$page = $book->pages->last();
|
$page = $book->pages->last();
|
||||||
$chapter = $book->chapters->last();
|
$chapter = $book->chapters->last();
|
||||||
|
|
||||||
|
@ -67,7 +68,7 @@ class EntitySearchTest extends TestCase
|
||||||
|
|
||||||
public function test_chapter_search()
|
public function test_chapter_search()
|
||||||
{
|
{
|
||||||
$chapter = \BookStack\Entities\Chapter::has('pages')->first();
|
$chapter = Chapter::has('pages')->first();
|
||||||
$page = $chapter->pages[0];
|
$page = $chapter->pages[0];
|
||||||
|
|
||||||
$pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));
|
$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()
|
public function test_tag_search()
|
||||||
{
|
{
|
||||||
$newTags = [
|
$newTags = [
|
||||||
new \BookStack\Actions\Tag([
|
new Tag([
|
||||||
'name' => 'animal',
|
'name' => 'animal',
|
||||||
'value' => 'cat'
|
'value' => 'cat'
|
||||||
]),
|
]),
|
||||||
new \BookStack\Actions\Tag([
|
new Tag([
|
||||||
'name' => 'color',
|
'name' => 'color',
|
||||||
'value' => 'red'
|
'value' => 'red'
|
||||||
])
|
])
|
||||||
|
@ -204,4 +205,75 @@ class EntitySearchTest extends TestCase
|
||||||
$chapterSearch->assertSee($chapter->name);
|
$chapterSearch->assertSee($chapter->name);
|
||||||
$chapterSearch->assertSee($chapter->book->getShortName(42));
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php namespace Tests;
|
<?php namespace Tests;
|
||||||
|
|
||||||
use BookStack\Notifications\TestEmail;
|
use BookStack\Notifications\TestEmail;
|
||||||
|
use Illuminate\Contracts\Notifications\Dispatcher;
|
||||||
use Illuminate\Support\Facades\Notification;
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
|
||||||
class TestEmailTest extends TestCase
|
class TestEmailTest extends TestCase
|
||||||
|
@ -26,6 +27,24 @@ class TestEmailTest extends TestCase
|
||||||
Notification::assertSentTo($admin, TestEmail::class);
|
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()
|
public function test_send_test_email_requires_settings_manage_permission()
|
||||||
{
|
{
|
||||||
Notification::fake();
|
Notification::fake();
|
||||||
|
|
Loading…
Reference in New Issue