Merge branch 'master' into release

This commit is contained in:
Dan Brown 2017-02-27 14:57:38 +00:00
commit a5d5904969
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
86 changed files with 2659 additions and 1446 deletions

View File

@ -1,5 +1,5 @@
dist: trusty
sudo: required
sudo: false
language: php
php:
- 7.0
@ -8,15 +8,11 @@ cache:
directories:
- $HOME/.composer/cache
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
before_script:
- mysql -u root -e 'create database `bookstack-test`;'
- mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- mysql -u root -e "FLUSH PRIVILEGES;"
- phpenv config-rm xdebug.ini
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
@ -25,5 +21,8 @@ before_script:
- php artisan migrate --force -n --database=mysql_testing
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
after_failure:
- cat storage/logs/laravel.log
script:
- phpunit

View File

@ -0,0 +1,47 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Activity;
use Illuminate\Console\Command;
class ClearActivity extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-activity';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear user activity from the system';
protected $activity;
/**
* Create a new command instance.
*
* @param Activity $activity
*/
public function __construct(Activity $activity)
{
$this->activity = $activity;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->activity->newQuery()->truncate();
$this->comment('System activity cleared');
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\PageRevision;
use Illuminate\Console\Command;
class ClearRevisions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-revisions
{--a|all : Include active update drafts in deletion}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear page revisions';
protected $pageRevision;
/**
* Create a new command instance.
*
* @param PageRevision $pageRevision
*/
public function __construct(PageRevision $pageRevision)
{
$this->pageRevision = $pageRevision;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
$this->comment('Revisions deleted');
}
}

View File

@ -4,21 +4,21 @@ namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
class ResetViews extends Command
class ClearViews extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'views:reset';
protected $signature = 'bookstack:clear-views';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset all view-counts for all entities.';
protected $description = 'Clear all view-counts for all entities.';
/**
* Create a new command instance.
@ -37,5 +37,6 @@ class ResetViews extends Command
public function handle()
{
\Views::resetAll();
$this->comment('Views cleared');
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
*
* @var string
*/
protected $signature = 'permissions:regen';
protected $signature = 'bookstack:regenerate-permissions';
/**
* The console command description.
@ -47,5 +47,6 @@ class RegeneratePermissions extends Command
public function handle()
{
$this->permissionService->buildJointPermissions();
$this->comment('Permissions regenerated');
}
}

View File

@ -13,8 +13,9 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
\BookStack\Console\Commands\Inspire::class,
\BookStack\Console\Commands\ResetViews::class,
\BookStack\Console\Commands\ClearViews::class,
\BookStack\Console\Commands\ClearActivity::class,
\BookStack\Console\Commands\ClearRevisions::class,
\BookStack\Console\Commands\RegeneratePermissions::class,
];
@ -26,7 +27,6 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->hourly();
//
}
}

View File

@ -3,9 +3,9 @@
namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PhpSpec\Exception\Example\ErrorException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;

View File

@ -3,6 +3,7 @@
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@ -12,16 +13,19 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
@ -258,4 +262,49 @@ class BookController extends Controller
session()->flash('success', trans('entities.books_permissions_updated'));
return redirect($book->getUrl());
}
/**
* Export a book as a PDF file.
* @param string $bookSlug
* @return mixed
*/
public function exportPdf($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$pdfContent = $this->exportService->bookToPdf($book);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
]);
}
/**
* Export a book as a contained HTML file.
* @param string $bookSlug
* @return mixed
*/
public function exportHtml($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToContainedHtml($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
]);
}
/**
* Export a book as a plain text file.
* @param $bookSlug
* @return mixed
*/
public function exportPlainText($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToPlainText($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
]);
}
}

View File

@ -3,6 +3,7 @@
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@ -12,16 +13,19 @@ class ChapterController extends Controller
protected $userRepo;
protected $entityRepo;
protected $exportService;
/**
* ChapterController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
@ -236,4 +240,52 @@ class ChapterController extends Controller
session()->flash('success', trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl());
}
/**
* Exports a chapter to pdf .
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
}
/**
* Export a chapter to a self-contained HTML file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
}
/**
* Export a chapter to a simple plaintext .txt file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPlainText($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
}
}

View File

@ -4,7 +4,7 @@ namespace BookStack\Http\Controllers;
use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;

View File

@ -369,10 +369,13 @@ class PageController extends Controller
public function showRevision($bookSlug, $pageSlug, $revisionId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $this->entityRepo->getById('page_revision', $revisionId, false);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
@ -390,7 +393,10 @@ class PageController extends Controller
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $this->entityRepo->getById('page_revision', $revisionId);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$prev = $revision->getPrevious();
$prevContent = ($prev === null) ? '' : $prev->html;
@ -423,7 +429,7 @@ class PageController extends Controller
}
/**
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
* @param string $bookSlug
* @param string $pageSlug
@ -433,7 +439,6 @@ class PageController extends Controller
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$pdfContent = $this->exportService->pageToPdf($page);
// return $pdfContent;
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'

View File

@ -1,5 +1,7 @@
<?php namespace BookStack\Providers;
use BookStack\Services\SettingService;
use BookStack\Setting;
use Illuminate\Support\ServiceProvider;
use Validator;
@ -17,6 +19,10 @@ class AppServiceProvider extends ServiceProvider
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
});
\Blade::directive('icon', function($expression) {
return "<?php echo icon($expression); ?>";
});
}
/**
@ -26,6 +32,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->singleton(SettingService::class, function($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
});
}
}

View File

@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class EventServiceProvider extends ServiceProvider
{
@ -13,8 +14,8 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'BookStack\Events\SomeEvent' => [
'BookStack\Listeners\EventListener',
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
],
];

View File

@ -1,36 +0,0 @@
<?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
class SocialiteServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) {
return new SocialiteManager($app);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['Laravel\Socialite\Contracts\Factory'];
}
}

View File

@ -86,8 +86,7 @@ class EntityRepo
$this->entities = [
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book,
'page_revision' => $this->pageRevision
'book' => $this->book
];
$this->viewService = $viewService;
$this->permissionService = $permissionService;
@ -314,11 +313,12 @@ class EntityRepo
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @param bool $filterDrafts
* @param bool $renderPages
* @return mixed
*/
public function getBookChildren(Book $book, $filterDrafts = false)
public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
{
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts)->get();
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
$entities = [];
$parents = [];
$tree = [];
@ -326,6 +326,10 @@ class EntityRepo
foreach ($q as $index => $rawEntity) {
if ($rawEntity->entity_type === 'BookStack\\Page') {
$entities[$index] = $this->page->newFromBuilder($rawEntity);
if ($renderPages) {
$entities[$index]->html = $rawEntity->description;
$entities[$index]->html = $this->renderPage($entities[$index]);
};
} else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
$entities[$index] = $this->chapter->newFromBuilder($rawEntity);
$key = $entities[$index]->entity_type . ':' . $entities[$index]->id;

View File

@ -1,5 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
@ -25,25 +27,105 @@ class ExportService
*/
public function pageToContainedHtml(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
$pageHtml = view('pages/export', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->containHtml($pageHtml);
}
/**
* Convert a page to a pdf file.
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToContainedHtml(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->containHtml($html);
}
/**
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
*/
public function bookToContainedHtml(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->containHtml($html);
}
/**
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
*/
public function pageToPdf(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
// return $pageHtml;
$html = view('pages/pdf', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToPdf(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a book to a PDF file
* @param Book $book
* @return string
*/
public function bookToPdf(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
*/
protected function htmlToPdf($html)
{
$containedHtml = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false;
$containedHtml = $this->containHtml($pageHtml);
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
$pdf = \PDF::loadHTML($containedHtml);
}
@ -123,6 +205,40 @@ class ExportService
return $text;
}
/**
* Convert a chapter into a plain text string.
* @param Chapter $chapter
* @return string
*/
public function chapterToPlainText(Chapter $chapter)
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
}
/**
* Convert a book into a plain text string.
* @param Book $book
* @return string
*/
public function bookToPlainText(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
$text .= $this->chapterToPlainText($bookChild);
} else {
$text .= $this->pageToPlainText($bookChild);
}
}
return $text;
}
}

View File

@ -41,7 +41,8 @@ class LdapService
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
$emailAttr = $this->config['email_attribute'];
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) return null;
$user = $users[0];
@ -49,7 +50,7 @@ class LdapService
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}

View File

@ -474,11 +474,13 @@ class PermissionService
/**
* Get the children of a book in an efficient single query, Filtered by the permission system.
* @param integer $book_id
* @param bool $filterDrafts
* @param bool $filterDrafts
* @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false) {
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageContentSelect = $fetchPageContent ? 'html' : "''";
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {

View File

@ -16,6 +16,7 @@ class SettingService
protected $setting;
protected $cache;
protected $localCache = [];
protected $cachePrefix = 'setting-';
@ -40,8 +41,12 @@ class SettingService
public function get($key, $default = false)
{
if ($default === false) $default = config('setting-defaults.' . $key, false);
if (isset($this->localCache[$key])) return $this->localCache[$key];
$value = $this->getValueFromStore($key, $default);
return $this->formatValue($value, $default);
$formatted = $this->formatValue($value, $default);
$this->localCache[$key] = $formatted;
return $formatted;
}
/**
@ -71,9 +76,8 @@ class SettingService
// Check the cache
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) return $cacheVal;
// Check the database
$settingObject = $this->getSettingObjectByKey($key);

View File

@ -14,7 +14,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
/**
* SocialAuthService constructor.
@ -181,14 +181,24 @@ class SocialAuthService
public function getActiveDrivers()
{
$activeDrivers = [];
foreach ($this->validSocialDrivers as $driverName) {
if ($this->checkDriverConfigured($driverName)) {
$activeDrivers[$driverName] = true;
foreach ($this->validSocialDrivers as $driverKey) {
if ($this->checkDriverConfigured($driverKey)) {
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
}
}
return $activeDrivers;
}
/**
* Get the presentational name for a driver.
* @param $driver
* @return mixed
*/
public function getDriverName($driver)
{
return config('services.' . strtolower($driver) . '.name');
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
@ -211,7 +221,6 @@ class SocialAuthService
*/
public function detachSocialAccount($socialDriver)
{
session();
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());

View File

@ -73,7 +73,7 @@ function userCan($permission, Ownable $ownable = null)
*/
function setting($key = null, $default = false)
{
$settingService = app(\BookStack\Services\SettingService::class);
$settingService = resolve(\BookStack\Services\SettingService::class);
if (is_null($key)) return $settingService;
return $settingService->get($key, $default);
}
@ -126,6 +126,16 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null)
return app('redirect')->to($to, $status, $headers, $secure);
}
function icon($name, $attrs = []) {
$iconPath = resource_path('assets/icons/' . $name . '.svg');
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
$attrString .= $attrName . '="' . $attr . '" ';
}
$fileContents = file_get_contents($iconPath);
return str_replace('<svg', '<svg' . $attrString, $fileContents);
}
/**
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction

View File

@ -6,17 +6,19 @@
"type": "project",
"require": {
"php": ">=5.6.4",
"laravel/framework": "^5.3.4",
"laravel/framework": "5.4.*",
"ext-tidy": "*",
"intervention/image": "^2.3",
"laravel/socialite": "^2.0",
"barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.2.3",
"laravel/socialite": "^3.0",
"barryvdh/laravel-ide-helper": "^2.2.3",
"barryvdh/laravel-debugbar": "^2.3.2",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.7",
"barryvdh/laravel-dompdf": "^0.8",
"predis/predis": "^1.1",
"gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1"
"barryvdh/laravel-snappy": "^0.3.1",
"laravel/browser-kit-testing": "^1.0",
"socialiteproviders/slack": "^3.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
@ -34,9 +36,9 @@
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [

1691
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -139,7 +139,7 @@ return [
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
/**
* Third Party

View File

@ -82,7 +82,7 @@ return [
'mysql_testing' => [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),

View File

@ -1,6 +1,6 @@
<?php
return array(
return [
/*
|--------------------------------------------------------------------------
@ -13,7 +13,7 @@ return array(
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => array(
'defines' => [
/**
* The location of the DOMPDF font directory
*
@ -143,7 +143,7 @@ return array(
* the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here.
*/
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen",
"DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
/**
* The default paper size.
@ -260,7 +260,7 @@ return array(
"DOMPDF_ENABLE_HTML5PARSER" => true,
),
],
);
];

View File

@ -41,12 +41,35 @@ return [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/github/callback',
'name' => 'GitHub',
],
'google' => [
'client_id' => env('GOOGLE_APP_ID', false),
'client_secret' => env('GOOGLE_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/google/callback',
'name' => 'Google',
],
'slack' => [
'client_id' => env('SLACK_APP_ID', false),
'client_secret' => env('SLACK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/slack/callback',
'name' => 'Slack',
],
'facebook' => [
'client_id' => env('FACEBOOK_APP_ID', false),
'client_secret' => env('FACEBOOK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
'name' => 'Facebook',
],
'twitter' => [
'client_id' => env('TWITTER_APP_ID', false),
'client_secret' => env('TWITTER_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
'name' => 'Twitter',
],
'ldap' => [
@ -55,7 +78,8 @@ return [
'pass' => env('LDAP_PASS', false),
'base_dn' => env('LDAP_BASE_DN', false),
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false)
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
]
];

View File

@ -11,14 +11,14 @@ class DummyContentSeeder extends Seeder
*/
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
$user = factory(\BookStack\User::class)->create();
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
$books = factory(BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
$books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);

View File

@ -1,7 +1,7 @@
# BookStack
[![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest)
[![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/releases/latest)
[![license](https://img.shields.io/github/license/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack)
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
@ -46,6 +46,12 @@ As part of BookStack v0.14 support for translations has been built in. All text
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
## Contributing
Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
Pull requests are very welcome. If the scope of your pull request is very large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
## Website, Docs & Blog
The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 266.893 266.895"><path fill="#3C5A99" d="M248.082 262.307c7.854 0 14.223-6.37 14.223-14.225V18.812c0-7.857-6.368-14.224-14.223-14.224H18.812c-7.857 0-14.224 6.367-14.224 14.224v229.27c0 7.855 6.366 14.225 14.224 14.225h229.27z"/><path fill="#FFF" d="M182.41 262.307v-99.803h33.498l5.016-38.895H182.41V98.775c0-11.26 3.126-18.935 19.274-18.935l20.596-.01V45.047c-3.562-.474-15.788-1.533-30.012-1.533-29.695 0-50.025 18.126-50.025 51.413v28.684h-33.585v38.894h33.585v99.803h40.166z"/></svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#333333" fill-rule="evenodd" d="M31.9.693c-17.672 0-32 14.327-32 32 0 14.14 9.17 26.132 21.886 30.365 1.6.293 2.184-.695 2.184-1.544 0-.758-.028-2.77-.043-5.44-8.9 1.932-10.78-4.292-10.78-4.292-1.455-3.695-3.553-4.68-3.553-4.68-2.905-1.985.22-1.946.22-1.946 3.212.228 4.9 3.3 4.9 3.3 2.856 4.888 7.492 3.476 9.315 2.66.29-2.07 1.11-3.48 2.03-4.28-7.11-.807-14.58-3.554-14.58-15.816 0-3.493 1.243-6.35 3.29-8.586-.33-.81-1.428-4.063.313-8.47 0 0 2.687-.86 8.8 3.28 2.552-.708 5.29-1.063 8.01-1.075 2.718.01 5.457.36 8.01 1.07 6.11-4.14 8.793-3.28 8.793-3.28 1.747 4.403.65 7.66.32 8.47 2.05 2.233 3.29 5.09 3.29 8.582 0 12.293-7.483 15-14.61 15.79 1.15.99 2.17 2.94 2.17 5.926 0 4.277-.04 7.73-.04 8.777 0 .857.578 1.853 2.2 1.54 12.71-4.235 21.87-16.22 21.87-30.355 0-17.674-14.326-32-32-32"/></svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g fill="none" fill-rule="evenodd"><path fill="#4285f4" d="M62.735 32.712c0-2.27-.204-4.45-.582-6.545H32.015v12.378h17.222c-.742 4-2.997 7.39-6.386 9.658v8.03h10.344c6.05-5.57 9.542-13.775 9.542-23.52z"/><path fill="#34a853" d="M32.015 63.985c8.64 0 15.883-2.865 21.178-7.753l-10.342-8.03c-2.863 1.92-6.53 3.056-10.834 3.056-8.335 0-15.39-5.63-17.906-13.193H3.417v8.29c5.266 10.46 16.088 17.63 28.597 17.63z"/><path fill="#fbbc05" d="M14.11 38.065c-.64-1.92-1.004-3.97-1.004-6.08s.363-4.16 1.003-6.08v-8.29H3.416C1.25 21.935.015 26.82.015 31.985c0 5.163 1.236 10.05 3.403 14.37l10.69-8.29z"/><path fill="#ea4335" d="M32.015 12.712c4.698 0 8.916 1.615 12.233 4.786l9.178-9.178C47.884 3.156 40.64-.015 32.016-.015c-12.51 0-23.332 7.17-28.598 17.63l10.69 8.29c2.518-7.563 9.572-13.193 17.907-13.193z"/><path d="M.015-.015h64v64h-64v-64z"/></g></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 64 64"><style id="style3">.st0{fill:#ECB32D;} .st1{fill:#63C1A0;} .st2{fill:#E01A59;} .st3{fill:#331433;} .st4{fill:#D62027;} .st5{fill:#89D3DF;} .st6{fill:#258B74;} .st7{fill:#819C3C;}</style><g id="g5"><g id="g7"><path id="path9" fill="#ecb32d" d="M41.478 3.945C40.48.95 37.28-.677 34.288.27c-2.992.997-4.62 4.2-3.674 7.195l14.748 45.383c.997 2.784 4.042 4.36 6.928 3.52 3.044-.893 4.88-4.098 3.884-7.04 0-.104-14.696-45.383-14.696-45.383z" class="st0"/><path id="path11" fill="#63c1a0" d="M18.648 11.352c-.997-2.994-4.2-4.623-7.19-3.677-2.992.998-4.62 4.202-3.674 7.196l14.748 45.39c.997 2.784 4.04 4.36 6.928 3.52 3.044-.894 4.88-4.098 3.883-7.04 0-.105-14.695-45.383-14.695-45.383z" class="st1"/><path id="path13" fill="#e01a59" d="M60.058 41.502c2.99-.998 4.618-4.202 3.674-7.196-.997-2.994-4.2-4.622-7.19-3.677L11.14 45.44c-2.78.998-4.356 4.045-3.516 6.934.892 3.046 4.094 4.885 7.033 3.887.104 0 45.398-14.76 45.398-14.76z" class="st2"/><path id="path15" fill="#331433" d="M20.59 54.372c2.94-.946 6.77-2.207 10.864-3.52-.945-2.94-2.204-6.776-3.516-10.873l-10.865 3.514L20.59 54.37z" class="st3"/><path id="path17" fill="#d62027" d="M43.473 46.913c4.094-1.313 7.925-2.574 10.864-3.52-.945-2.94-2.204-6.776-3.516-10.873l-10.86 3.52 3.518 10.873z" class="st4"/><path id="path19" fill="#89d3df" d="M52.605 18.653c2.992-.998 4.62-4.202 3.674-7.196-1-2.994-4.2-4.623-7.19-3.677L3.74 22.54c-2.78.998-4.356 4.045-3.516 6.934.892 3.046 4.094 4.885 7.033 3.887.104 0 45.345-14.703 45.345-14.703z" class="st5"/><path id="path21" fill="#258b74" d="M13.19 31.47c2.94-.946 6.77-2.206 10.864-3.52-1.312-4.097-2.572-7.93-3.517-10.873l-10.864 3.52L13.19 31.47z" class="st6"/><path id="path23" fill="#819c3c" d="M36.02 24.063c4.094-1.313 7.925-2.573 10.864-3.52-1.312-4.096-2.57-7.93-3.516-10.872l-10.864 3.52 3.516 10.877z" class="st7"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#00aced" d="M64 12.145c-2.355 1.045-4.886 1.75-7.54 2.068 2.71-1.625 4.79-4.198 5.772-7.265-2.538 1.505-5.347 2.598-8.338 3.187-2.395-2.552-5.808-4.147-9.584-4.147-7.252 0-13.13 5.88-13.13 13.13 0 1.03.115 2.032.34 2.993-10.914-.543-20.59-5.77-27.065-13.714-1.13 1.94-1.777 4.195-1.777 6.6 0 4.556 2.317 8.575 5.84 10.93-2.15-.068-4.176-.66-5.946-1.642v.166c0 6.36 4.525 11.667 10.53 12.874-1.1.3-2.26.46-3.458.46-.846 0-1.67-.08-2.47-.234 1.67 5.215 6.52 9.012 12.265 9.117-4.498 3.522-10.16 5.62-16.31 5.62-1.06 0-2.107-.06-3.13-.183C5.81 55.827 12.71 58 20.124 58c24.15 0 37.358-20.008 37.358-37.36 0-.568-.013-1.134-.038-1.698 2.566-1.85 4.792-4.163 6.552-6.797"/></svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@ -214,6 +214,19 @@ export default function (ngApp, events) {
}
}]);
let renderer = new markdown.Renderer();
// Custom markdown checkbox list item
// Attribution: https://github.com/chjj/marked/issues/107#issuecomment-44542001
renderer.listitem = function(text) {
if (/^\s*\[[x ]\]\s*/.test(text)) {
text = text
.replace(/^\s*\[ \]\s*/, '<input type="checkbox"/>')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" checked/>');
return `<li class="checkbox-item">${text}</li>`;
}
return `<li>${text}</li>`;
};
/**
* Markdown input
* Handles the logic for just the editor input field.
@ -231,13 +244,13 @@ export default function (ngApp, events) {
element = element.find('textarea').first();
let content = element.val();
scope.mdModel = content;
scope.mdChange(markdown(content));
scope.mdChange(markdown(content, {renderer: renderer}));
element.on('change input', (event) => {
content = element.val();
$timeout(() => {
scope.mdModel = content;
scope.mdChange(markdown(content));
scope.mdChange(markdown(content, {renderer: renderer}));
});
});

View File

@ -135,11 +135,19 @@
border-left: 3px solid #BBB;
background-color: #EEE;
padding: $-s;
padding-left: $-xl;
display: block;
position: relative;
&:before {
font-family: 'Material-Design-Iconic-Font';
padding-right: $-s;
left: $-xs + 4px;
top: 50%;
margin-top: -9px;
//top: $-xs + 5px;
display: inline-block;
position: absolute;
font-size: 1.222em;
line-height: 1;
}
&.success {
border-left-color: $positive;

View File

@ -54,6 +54,9 @@ $button-border-radius: 2px;
&.muted {
@include generate-button-colors(#EEE, #888);
}
&.muted-light {
@include generate-button-colors(#666, #e4e4e4);
}
}
.text-button {
@ -92,6 +95,9 @@ $button-border-radius: 2px;
width: 100%;
text-align: center;
display: block;
&.text-left {
text-align: left;
}
}
.button.icon {
@ -100,6 +106,19 @@ $button-border-radius: 2px;
}
}
.button.svg {
svg {
display: inline-block;
position: absolute;
left: $-m;
top: $-s - 2px;
width: 24px;
}
padding: $-s $-m;
padding-bottom: $-s - 2px;
padding-left: $-m*2 + 24px;
}
.button[disabled] {
background-color: #BBB;
cursor: default;

View File

@ -55,20 +55,6 @@ div[class^="col-"] img {
}
}
.center-box {
margin: $-xl auto 0 auto;
padding: $-m $-xxl $-xl*2 $-xxl;
max-width: 346px;
display: inline-block;
text-align: left;
vertical-align: top;
&.login {
background-color: #EEE;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #DDD;
}
}
.row {
margin-left: -$-m;
margin-right: -$-m;

View File

@ -16,7 +16,7 @@ h2 {
}
h3 {
font-size: 2.333em;
line-height: 1.571428572em;
line-height: 1.221428572em;
margin-top: 0.78571429em;
margin-bottom: 0.43137255em;
}
@ -71,6 +71,13 @@ a, .link {
padding-right: 0;
padding-left: $-s;
}
&.icon {
display: inline-block;
}
svg {
position: relative;
display: inline-block;
}
}
/*
@ -84,7 +91,6 @@ p, ul, ol, pre, table, blockquote {
hr {
border: 0;
height: 1px;
border: 0;
background: #EAEAEA;
margin-bottom: $-l;
&.faded {
@ -275,6 +281,14 @@ ol {
overflow: hidden;
}
li.checkbox-item {
list-style: none;
margin-left: - ($-m * 1.3);
input[type="checkbox"] {
margin-right: $-xs;
}
}
/*
* Generic text styling classes
*/

View File

@ -1,4 +1,4 @@
@import "reset";
//@import "reset";
@import "variables";
@import "mixins";
@import "html";

View File

@ -251,10 +251,24 @@ $btt-size: 40px;
}
}
.center-box {
margin: $-xl auto 0 auto;
padding: $-m $-xxl $-xl $-xxl;
width: 420px;
max-width: 100%;
display: inline-block;
text-align: left;
vertical-align: top;
//border: 1px solid #DDD;
input {
width: 100%;
}
&.login {
background-color: #EEE;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #DDD;
}
}

View File

@ -14,7 +14,50 @@ return [
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.',
/**
* Email Confirmation Text
* Login & Register
*/
'sign_up' => 'Registrieren',
'log_in' => 'Anmelden',
'logout' => 'Abmelden',
'name' => 'Name',
'username' => 'Benutzername',
'email' => 'E-Mail',
'password' => 'Passwort',
'password_confirm' => 'Passwort best&auml;tigen',
'password_hint' => 'Mindestlänge: 5 Zeichen',
'forgot_password' => 'Passwort vergessen?',
'remember_me' => 'Angemeldet bleiben',
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
'create_account' => 'Account anlegen',
'social_login' => 'Social Login',
'social_registration' => 'Social Registrierung',
'social_registration_text' => 'Mit einem dieser Möglichkeiten registrieren oder anmelden.',
'register_thanks' => 'Vielen Dank für Ihre Registrierung!',
'register_confirm' => 'Bitte prüfen Sie Ihren E-Mail Eingang und klicken auf den Verifizieren-Button, um :appName nutzen zu können.',
'registrations_disabled' => 'Die Registrierung ist momentan nicht möglich',
'registration_email_domain_invalid' => 'Diese E-Mail-Domain ist für die Benutzer der Applikation nicht freigeschaltet.',
'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.',
/**
* Password Reset
*/
'reset_password' => 'Passwort vergessen',
'reset_password_send_instructions' => 'Bitte geben Sie unten Ihre E-Mail-Adresse ein und Sie erhalten eine E-Mail, um Ihr Passwort zurück zu setzen.',
'reset_password_send_button' => 'Passwort zurücksetzen',
'reset_password_sent_success' => 'Eine E-Mail mit den Instruktionen, um Ihr Passwort zurückzusetzen wurde an :email gesendet.',
'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurück gesetzt.',
'email_reset_subject' => 'Passwort zurücksetzen für :appName',
'email_reset_text' => 'Sie erhalten diese E-Mail, weil eine Passwort-Rücksetzung für Ihren Account beantragt wurde.',
'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Best&auml;tigen sie ihre E-Mail Adresse bei :appName',
'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!',
@ -23,4 +66,10 @@ return [
'email_confirm_send_error' => 'Best&auml;tigungs-E-Mail ben&ouml;tigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.',
'email_confirm_success' => 'Ihre E-Mail Adresse wurde best&auml;tigt!',
'email_confirm_resent' => 'Best&auml;tigungs-E-Mail wurde erneut versendet, bitte &uuml;berpr&uuml;fen sie ihren Posteingang.',
'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',
'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.',
'email_not_confirmed_click_link' => 'Bitte klicken Sie auf den Link in der E-Mail, die Sie nach der Registrierung erhalten haben.',
'email_not_confirmed_resend' => 'Wenn Sie die E-Mail nicht erhalten haben, können Sie die Nachricht erneut anfordern. Füllen Sie hierzu bitte das folgende Formular aus:',
'email_not_confirmed_resend_button' => 'Bestätigungs E-Mail erneut senden',
];

View File

@ -0,0 +1,58 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Abbrechen',
'confirm' => 'Bestätigen',
'back' => 'Zurück',
'save' => 'Speichern',
'continue' => 'Weiter',
'select' => 'Auswählen',
/**
* Form Labels
*/
'name' => 'Name',
'description' => 'Beschreibung',
'role' => 'Rolle',
/**
* Actions
*/
'actions' => 'Aktionen',
'view' => 'Anzeigen',
'create' => 'Anlegen',
'update' => 'Aktualisieren',
'edit' => 'Bearbeiten',
'sort' => 'Sortieren',
'move' => 'Verschieben',
'delete' => 'L&ouml;schen',
'search' => 'Suchen',
'search_clear' => 'Suche l&ouml;schen',
'reset' => 'Zurücksetzen',
'remove' => 'Entfernen',
/**
* Misc
*/
'deleted_user' => 'Gel&ouml;schte Benutzer',
'no_activity' => 'Keine Aktivit&auml;ten zum Anzeigen',
'no_items' => 'Keine Eintr&auml;ge gefunden.',
'back_to_top' => 'nach oben',
'toggle_details' => 'Details zeigen/verstecken',
/**
* Header
*/
'view_profile' => 'Profil ansehen',
'edit_profile' => 'Profil bearbeiten',
/**
* Email Content
*/
'email_action_help' => 'Sollte es beim Anklicken des ":actionText" Buttons Probleme geben, kopieren Sie folgende URL und fügen diese in Ihrem Webbrowser ein:',
'email_rights' => 'Alle Rechte vorbehalten',
];

View File

@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Bild auswählen',
'image_all' => 'Alle',
'image_all_title' => 'Alle Bilder anzeigen',
'image_book_title' => 'Zeige alle Bilder, die in dieses Buch hochgeladen wurden',
'image_page_title' => 'Zeige alle Bilder, die auf diese Seite hochgeladen wurden',
'image_search_hint' => 'Nach Bildnamen suchen',
'image_uploaded' => 'Hochgeladen am :uploadedDate',
'image_load_more' => 'Mehr',
'image_image_name' => 'Bildname',
'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild tatsächlich entfernen möchten.',
'image_select_image' => 'Bild auswählen',
'image_dropzone' => 'Ziehen Sie Bilder hier hinein oder klicken Sie hier, um ein Bild auszuwählen',
'images_deleted' => 'Bilder gelöscht',
'image_preview' => 'Bildvorschau',
'image_upload_success' => 'Bild erfolgreich hochgeladen',
'image_update_success' => 'Bilddetails erfolgreich aktualisiert',
'image_delete_success' => 'Bild erfolgreich gelöscht'
];

View File

@ -0,0 +1,225 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'K&uuml;rzlich angelegt',
'recently_created_pages' => 'K&uuml;rzlich angelegte Seiten',
'recently_updated_pages' => 'K&uuml;rzlich aktualisierte Seiten',
'recently_created_chapters' => 'K&uuml;rzlich angelegte Kapitel',
'recently_created_books' => 'K&uuml;rzlich angelegte B&uuml;cher',
'recently_update' => 'K&uuml;rzlich aktualisiert',
'recently_viewed' => 'K&uuml;rzlich angesehen',
'recent_activity' => 'K&uuml;rzliche Aktivit&auml;t',
'create_now' => 'Jetzt anlegen',
'revisions' => 'Revisionen',
'meta_created' => 'Angelegt am :timeLength',
'meta_created_name' => 'Angelegt am :timeLength durch :user',
'meta_updated' => 'Aktualisiert am :timeLength',
'meta_updated_name' => 'Aktualisiert am :timeLength durch :user',
'x_pages' => ':count Seiten',
'entity_select' => 'Eintrag ausw&auml;hlen',
'images' => 'Bilder',
'my_recent_drafts' => 'Meine k&uuml;rzlichen Entw&uuml;rfe',
'my_recently_viewed' => 'K&uuml;rzlich von mir angesehen',
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.',
/**
* Permissions and restrictions
*/
'permissions' => 'Berechtigungen',
'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, &uuml;berschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
'permissions_save' => 'Berechtigungen speichern',
/**
* Search
*/
'search_results' => 'Suchergebnisse',
'search_results_page' => 'Seiten-Suchergebnisse',
'search_results_chapter' => 'Kapitel-Suchergebnisse',
'search_results_book' => 'Buch-Suchergebnisse',
'search_clear' => 'Suche zur&uuml;cksetzen',
'search_view_pages' => 'Zeige alle passenden Seiten',
'search_view_chapters' => 'Zeige alle passenden Kapitel',
'search_view_books' => 'Zeige alle passenden B&uuml;cher',
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
'search_for_term' => 'Suche nach :term',
'search_page_for_term' => 'Suche nach :term in Seiten',
'search_chapter_for_term' => 'Suche nach :term in Kapiteln',
'search_book_for_term' => 'Suche nach :term in B&uuml;chern',
/**
* Books
*/
'book' => 'Buch',
'books' => 'B&uuml;cher',
'books_empty' => 'Es wurden keine B&uuml;cher angelegt',
'books_popular' => 'Popul&auml;re B&uuml;cher',
'books_recent' => 'K&uuml;rzlich genutzte B&uuml;cher',
'books_popular_empty' => 'Die popul&auml;rsten B&uuml;cher werden hier angezeigt.',
'books_create' => 'Neues Buch anlegen',
'books_delete' => 'Buch l&ouml;schen',
'books_delete_named' => 'Buch :bookName l&ouml;schen',
'books_delete_explain' => 'Sie m&ouml;chten das Buch \':bookName\' l&ouml;schen und alle Seiten und Kapitel entfernen.',
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch l&ouml;schen m&ouml;chten?',
'books_edit' => 'Buch bearbeiten',
'books_edit_named' => 'Buch :bookName bearbeiten',
'books_form_book_name' => 'Buchname',
'books_save' => 'Buch speichern',
'books_permissions' => 'Buch-Berechtigungen',
'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',
'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel f&uuml;r dieses Buch angelegt.',
'books_empty_create_page' => 'Neue Seite anlegen',
'books_empty_or' => 'oder',
'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',
'books_empty_add_chapter' => 'Neues Kapitel hinzuf&uuml;gen',
'books_permissions_active' => 'Buch-Berechtigungen aktiv',
'books_search_this' => 'Dieses Buch durchsuchen',
'books_navigation' => 'Buch-Navigation',
'books_sort' => 'Buchinhalte sortieren',
'books_sort_named' => 'Buch :bookName sortieren',
'books_sort_show_other' => 'Andere B&uuml;cher zeigen',
'books_sort_save' => 'Neue Reihenfolge speichern',
/**
* Chapters
*/
'chapter' => 'Kapitel',
'chapters' => 'Kapitel',
'chapters_popular' => 'Popul&auml;re Kapitel',
'chapters_new' => 'Neues Kapitel',
'chapters_create' => 'Neues Kapitel anlegen',
'chapters_delete' => 'Kapitel entfernen',
'chapters_delete_named' => 'Kapitel :chapterName entfernen',
'chapters_delete_explain' => 'Sie m&ouml;chten das Kapitel \':chapterName\' l&ouml;schen und alle Seiten dem direkten Eltern-Buch hinzugef&uuml;gen.',
'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel l&ouml;schen m&ouml;chten?',
'chapters_edit' => 'Kapitel bearbeiten',
'chapters_edit_named' => 'Kapitel :chapterName bearbeiten',
'chapters_save' => 'Kapitel speichern',
'chapters_move' => 'Kapitel verschieben',
'chapters_move_named' => 'Kapitel :chapterName verschieben',
'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.',
'chapters_permissions' => 'Kapitel-Berechtigungen',
'chapters_empty' => 'Aktuell sind keine Kapitel in diesem Buch angelegt.',
'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
/**
* Pages
*/
'page' => 'Seite',
'pages' => 'Seiten',
'pages_popular' => 'Popul&auml;re Seiten',
'pages_new' => 'Neue Seite',
'pages_attachments' => 'Anh&auml;nge',
'pages_navigation' => 'Seitennavigation',
'pages_delete' => 'Seite l&ouml;schen',
'pages_delete_named' => 'Seite :pageName l&ouml;schen',
'pages_delete_draft_named' => 'Seitenentwurf von :pageName l&ouml;schen',
'pages_delete_draft' => 'Seitenentwurf l&ouml;schen',
'pages_delete_success' => 'Seite gel&ouml;scht',
'pages_delete_draft_success' => 'Seitenentwurf gel&ouml;scht',
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite l&ouml;schen m&ouml;chen?',
'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf l&ouml;schen m&ouml;chten?',
'pages_editing_named' => 'Seite :pageName bearbeiten',
'pages_edit_toggle_header' => 'Toggle header',
'pages_edit_save_draft' => 'Entwurf speichern',
'pages_edit_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_page' => 'Seite bearbeiten',
'pages_edit_draft_save_at' => 'Entwurf gespeichert um ',
'pages_edit_delete_draft' => 'Entwurf l&ouml;schen',
'pages_edit_discard_draft' => 'Entwurf verwerfen',
'pages_edit_set_changelog' => 'Ver&auml;nderungshinweis setzen',
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer &Auml;nderungen ein',
'pages_edit_enter_changelog' => 'Ver&auml;nderungshinweis eingeben',
'pages_save' => 'Seite speichern',
'pages_title' => 'Seitentitel',
'pages_name' => 'Seitenname',
'pages_md_editor' => 'Redakteur',
'pages_md_preview' => 'Vorschau',
'pages_md_insert_image' => 'Bild einf&uuml;gen',
'pages_md_insert_link' => 'Link zu einem Objekt einf&uuml;gen',
'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
'pages_move' => 'Seite verschieben',
'pages_move_success' => 'Seite nach ":parentName" verschoben',
'pages_permissions' => 'Seiten Berechtigungen',
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
'pages_revisions' => 'Seitenversionen',
'pages_revisions_named' => 'Seitenversionen von :pageName',
'pages_revision_named' => 'Seitenversion von :pageName',
'pages_revisions_created_by' => 'Angelegt von',
'pages_revisions_date' => 'Versionsdatum',
'pages_revisions_changelog' => 'Ver&auml;nderungshinweise',
'pages_revisions_changes' => 'Ver&auml;nderungen',
'pages_revisions_current' => 'Aktuelle Version',
'pages_revisions_preview' => 'Vorschau',
'pages_revisions_restore' => 'Zur&uuml;ck sichern',
'pages_revisions_none' => 'Diese Seite hat keine &auml;lteren Versionen.',
'pages_export' => 'Exportieren',
'pages_export_html' => 'HTML-Datei',
'pages_export_pdf' => 'PDF-Datei',
'pages_export_text' => 'Text-Datei',
'pages_copy_link' => 'Link kopieren',
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
'pages_initial_revision' => 'Erste Ver&ouml;ffentlichung',
'pages_initial_name' => 'Neue Seite',
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.',
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt ver&auml;ndert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
'pages_draft_edit_active' => [
'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.',
'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.',
'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',
'time_b' => 'in den letzten :minCount Minuten',
'message' => ':start :time. Achten Sie darauf keine Aktualisierungen von anderen Benutzern zu &uuml;berschreiben!',
],
'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.',
/**
* Editor sidebar
*/
'page_tags' => 'Seiten-Schlagw&ouml;rter',
'tag' => 'Schlagwort',
'tags' => 'Schlagworte',
'tag_value' => 'Schlagwortinhalt (Optional)',
'tags_explain' => "F&uuml;gen Sie Schlagworte hinzu, um Ihren Inhalt zu kategorisieren. \n Sie k&ouml;nnen einen erkl&auml;renden Inhalt hinzuf&uuml;gen, um eine genauere Unterteilung vorzunehmen.",
'tags_add' => 'Weiteres Schlagwort hinzuf&uuml;gen',
'attachments' => 'Anh&auml;nge',
'attachments_explain' => 'Sie k&ouml;nnen auf Ihrer Seite Dateien hochladen oder Links anf&uuml;gen. Diese werden in der seitlich angezeigt.',
'attachments_explain_instant_save' => '&Auml;nderungen werden direkt gespeichert.',
'attachments_items' => 'Angef&uuml;gte Elemente',
'attachments_upload' => 'Datei hochladen',
'attachments_link' => 'Link anf&uuml;gen',
'attachments_set_link' => 'Link setzen',
'attachments_delete_confirm' => 'Klicken Sie erneut auf l&ouml;schen, um diesen Anhang zu entfernen.',
'attachments_dropzone' => 'Ziehen Sie Dateien hier hinein oder klicken Sie hier, um eine Datei auszuw&auml;hlen',
'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
'attachments_explain_link' => 'Wenn Sie keine Datei hochladen m&ouml;chten, k&ouml;nnen Sie stattdessen einen Link anf&uuml;gen. Dieser Link kann auf eine andere Seite oder zu einer Datei in der Cloud weisen.',
'attachments_link_name' => 'Link-Name',
'attachment_link' => 'Link zum Anhang',
'attachments_link_url' => 'Link zu einer Datei',
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
'attach' => 'anf&uuml;gen',
'attachments_edit_file' => 'Datei bearbeiten',
'attachments_edit_file_name' => 'Dateiname',
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu &uuml;berschreiben',
'attachments_order_updated' => 'Reihenfolge der Anh&auml;nge aktualisiert',
'attachments_updated_success' => 'Anhang-Details aktualisiert',
'attachments_deleted' => 'Anhang gel&ouml;scht',
'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen',
'attachments_file_updated' => 'Datei erfolgreich aktualisisert',
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugef&uuml;gt',
/**
* Profile View
*/
'profile_user_for_x' => 'Benutzer seit :time',
'profile_created_content' => 'Angelegte Inhalte',
'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.',
'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.',
'profile_not_created_books' => ':userName hat bisher keine B&uuml;cher angelegt.',
];

View File

@ -8,5 +8,63 @@ return [
// Pages
'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.'
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.',
// Auth
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.',
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits best&auml;tigt. Bitte melden Sie sich an.',
'email_confirmation_invalid' => 'Der Best&auml;tigungs-Token ist nicht g&uuml;ltig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
'email_confirmation_expired' => 'Der Best&auml;tigungs-Token ist abgelaufen. Es wurde eine neue Best&auml;tigungs-E-Mail gesendet.',
'ldap_fail_anonymous' => 'Anonymer LDAP Zugriff ist fehlgeschlafgen',
'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen',
'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.',
'ldap_cannot_connect' => 'Die Verbindung zu LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
'social_no_action_defined' => 'Es ist keine Aktion definiert',
'social_account_in_use' => 'Dieses :socialAccount Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount Konto an.',
'social_account_email_in_use' => 'Die E-Mail-Adresse :email ist bereits registriert. Wenn Sie bereits registriert sind, k&ouml;nnen Sie Ihr :socialAccount Konto in Ihren Profil-Einstellungen verkn&uuml;pfen.',
'social_account_existing' => 'Dieses :socialAccount Konto ist bereits mit Ihrem Profil verkn&uuml;pft.',
'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.',
'social_account_not_used' => 'Dieses :socialAccount Konto ist bisher keinem Benutzer zugeordnet. Bitte verkn&uuml;pfen Sie deses in Ihrem Profil-Einstellungen.',
'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen k&ouml;nnen Sie ein solches Konto mit der :socialAccount Option anlegen.',
'social_driver_not_found' => 'Social-Media Konto Treiber nicht gefunden',
'social_driver_not_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.',
// System
'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.',
'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte pr&uuml;fen Sie, ob Sie die GD PHP Erweiterung installiert haben.',
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigr&ouml;&szlig;e. Bitte versuchen Sie es mit einer kleineren Datei.',
'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
// Attachments
'attachment_page_mismatch' => 'Die Seite stimmt nach dem Hochladen des Anhangs nicht &uuml;berein.',
// Pages
'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
// Entities
'entity_not_found' => 'Eintrag nicht gefunden',
'book_not_found' => 'Buch nicht gefunden',
'page_not_found' => 'Seite nicht gefunden',
'chapter_not_found' => 'Kapitel nicht gefunden',
'selected_book_not_found' => 'Das gew&auml;hlte Buch wurde nicht gefunden.',
'selected_book_chapter_not_found' => 'Das gew&auml;hlte Buch oder Kapitel wurde nicht gefunden.',
'guests_cannot_save_drafts' => 'G&auml;ste k&ouml;nnen keine Entw&uuml;rfe speichern',
// Users
'users_cannot_delete_only_admin' => 'Sie k&ouml;nnen den einzigen Administrator nicht l&ouml;schen.',
'users_cannot_delete_guest' => 'Sie k&ouml;nnen den Gast-Benutzer nicht l&ouml;schen',
// Roles
'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',
'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gel&ouml;scht werden',
'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gel&ouml;scht werden solange sie als Standardrolle f&uuml;r neue Registrierungen gesetzt ist',
// Error pages
'404_page_not_found' => 'Seite nicht gefunden',
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.',
'return_home' => 'Zur&uuml;ck zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten',
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
'back_soon' => 'Wir werden so schnell wie m&ouml;glich wieder online sein.',
];

View File

@ -16,7 +16,7 @@ return [
'password' => 'Pass&ouml;rter m&uuml;ssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.',
'user' => "Wir k&ouml;nnen keinen Benutzer mit dieser E-Mail Adresse finden.",
'token' => 'Dieser Passwort-Reset-Token ist ung&uuml;ltig.',
'sent' => 'Wir haben ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!',
'sent' => 'Wir haben Ihnen eine E-Mail mit einem Link zum Zur&uuml;cksetzen des Passworts zugesendet!',
'reset' => 'Ihr Passwort wurde zur&uuml;ckgesetzt!',
];

View File

@ -10,14 +10,19 @@ return [
'settings' => 'Einstellungen',
'settings_save' => 'Einstellungen speichern',
'settings_save_success' => 'Einstellungen gespeichert',
/**
* App settings
*/
'app_settings' => 'Anwendungseinstellungen',
'app_name' => 'Anwendungsname',
'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.',
'app_name_header' => 'Anwendungsname im Header anzeigen?',
'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?',
'app_secure_images' => 'Erh&ouml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',
'app_editor' => 'Seiteneditor',
'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
'app_custom_html' => 'Benutzerdefinierter HTML <head> Inhalt',
@ -25,15 +30,82 @@ return [
'app_logo' => 'Anwendungslogo',
'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein. <br>Gr&ouml;&szlig;ere Bilder werden verkleinert.',
'app_primary_color' => 'Prim&auml;re Anwendungsfarbe',
'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>Leer lassen des Feldes setzt auf die Standard-Anwendungsfarbe zur&uuml;ck.',
'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zur&uuml;ckgesetzt.',
/**
* Registration settings
*/
'reg_settings' => 'Registrierungseinstellungen',
'reg_allow' => 'Registrierung erlauben?',
'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
'reg_confirm_email' => 'Best&auml;tigung per E-Mail erforderlich?',
'reg_confirm_email_desc' => 'Falls die Einschr&auml;nkung f&uumlr; Domains genutzt wird, ist die Best&auml;tigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
'reg_confirm_email_desc' => 'Falls die Einschr&auml;nkung f&uuml;r Domains genutzt wird, ist die Best&auml;tigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschr&auml;nken',
'reg_confirm_restrict_domain_desc' => 'F&uuml;gen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschr&auml;nkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu best&auml;tigen, bevor sie diese Anwendung nutzen k&ouml;nnen. <br> Hinweis: Benutzer k&ouml;nnen ihre E-Mail Adresse nach erfolgreicher Registrierung &auml;ndern.',
'reg_confirm_restrict_domain_placeholder' => 'Keine Einschr&auml;nkung gesetzt',
/**
* Role settings
*/
'roles' => 'Rollen',
'role_user_roles' => 'Benutzer-Rollen',
'role_create' => 'Neue Rolle anlegen',
'role_create_success' => 'Rolle erfolgreich angelegt',
'role_delete' => 'Rolle l&ouml;schen',
'role_delete_confirm' => 'Sie m&ouml;chten die Rolle \':roleName\' l&ouml;schen.',
'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie k&ouml;nnen unten eine neue Rolle ausw&auml;hlen, die Sie diesen Benutzern zuordnen m&ouml;chten.',
'role_delete_no_migration' => "Den Benutzern keine andere Rolle zuordnen",
'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle l&ouml;schen m&ouml;chten?',
'role_delete_success' => 'Rolle erfolgreich gel&ouml;scht',
'role_edit' => 'Rolle bearbeiten',
'role_details' => 'Rollen-Details',
'role_name' => 'Rollenname',
'role_desc' => 'Kurzbeschreibung der Rolle',
'role_system' => 'System-Berechtigungen',
'role_manage_users' => 'Benutzer verwalten',
'role_manage_roles' => 'Rollen & Rollen-Berechtigungen verwalten',
'role_manage_entity_permissions' => 'Alle Buch-, Kapitel und Seiten-Berechtigungen verwalten',
'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener B&uuml;cher, Kapitel und Seiten verwalten',
'role_manage_settings' => 'Globaleinstellungen verwalrten',
'role_asset' => 'Berechtigungen',
'role_asset_desc' => 'Diese Berechtigungen gelten f&uuml;r den Standard-Zugriff innerhalb des Systems. Berechtigungen f&uuml;r B&uuml;cher, Kapitel und Seiten &uuml;berschreiben diese Berechtigungenen.',
'role_all' => 'Alle',
'role_own' => 'Eigene',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Rolle speichern',
'role_update_success' => 'Rolle erfolgreich gespeichert',
'role_users' => 'Dieser Rolle zugeordnete Benutzer',
'role_users_none' => 'Bisher sind dieser Rolle keiner Benutzer zugeordnet,',
/**
* Users
*/
'users' => 'Benutzer',
'user_profile' => 'Benutzerprofil',
'users_add_new' => 'Benutzer hinzuf&uuml;gen',
'users_search' => 'Benutzer suchen',
'users_role' => 'Benutzerrollen',
'users_external_auth_id' => 'Externe Authentifizierungs-ID',
'users_password_warning' => 'F&uuml;llen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort &auml;ndern m&ouml;chten:',
'users_system_public' => 'Dieser Benutzer repr&auml;sentiert alle Gast-Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
'users_delete' => 'Benutzer l&ouml;schen',
'users_delete_named' => 'Benutzer :userName l&ouml;schen',
'users_delete_warning' => 'Sie m&ouml;chten den Benutzer \':userName\' g&auml;nzlich aus dem System l&ouml;schen.',
'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer l&ouml;schen m&ouml;chten?',
'users_delete_success' => 'Benutzer erfolgreich gel&ouml;scht.',
'users_edit' => 'Benutzer bearbeiten',
'users_edit_profile' => 'Profil bearbeiten',
'users_edit_success' => 'Benutzer erfolgreich aktualisisert',
'users_avatar' => 'Benutzer-Bild',
'users_avatar_desc' => 'Dieses Bild sollte einen Durchmesser von ca. 256px haben.',
'users_preferred_language' => 'Bevorzugte Sprache',
'users_social_accounts' => 'Social-Media Konten',
'users_social_accounts_info' => 'Hier k&ouml;nnen Sie andere Social-Media Konten f&uuml;r eine schnellere und einfachere Anmeldung verkn&uuml;pfen. Wenn Sie ein Social-Media Konto hier l&ouml;sen, bleibt der Zugriff erhalteb. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verkn&uuml;pften Social-Media Kontos.',
'users_social_connect' => 'Social-Media Konto verkn&uuml;pfen',
'users_social_disconnect' => 'Social-Media Kontoverkn&uuml;pfung l&ouml;sen',
'users_social_connected' => ':socialAccount Konto wurde erfolgreich mit dem Profil verkn&uuml;pft.',
'users_social_disconnected' => ':socialAccount Konto wurde erfolgreich vom Profil gel&ouml;st.',
];

View File

@ -18,6 +18,8 @@ return [
*/
'sign_up' => 'Sign up',
'log_in' => 'Log in',
'log_in_with' => 'Login with :socialDriver',
'sign_up_with' => 'Sign up with :socialDriver',
'logout' => 'Logout',
'name' => 'Name',

View File

@ -32,13 +32,12 @@
@if(count($socialDrivers) > 0)
<hr class="margin-top">
<h3 class="text-muted">{{ trans('auth.social_login') }}</h3>
@if(isset($socialDrivers['google']))
<a id="social-login-google" href="{{ baseUrl("/login/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
@endif
@if(isset($socialDrivers['github']))
<a id="social-login-github" href="{{ baseUrl("/login/service/github") }}" style="color:#444;"><i class="zmdi zmdi-github zmdi-hc-4x"></i></a>
@endif
@foreach($socialDrivers as $driver => $name)
<a id="social-login-{{$driver}}" class="button block muted-light svg text-left" href="{{ baseUrl("/login/service/" . $driver) }}">
@icon($driver)
{{ trans('auth.log_in_with', ['socialDriver' => $name]) }}
</a>
@endforeach
@endif
</div>
</div>

View File

@ -35,14 +35,12 @@
@if(count($socialDrivers) > 0)
<hr class="margin-top">
<h3 class="text-muted">{{ trans('auth.social_registration') }}</h3>
<p class="text-small">{{ trans('auth.social_registration_text') }}</p>
@if(isset($socialDrivers['google']))
<a href="{{ baseUrl("/register/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
@endif
@if(isset($socialDrivers['github']))
<a href="{{ baseUrl("/register/service/github") }}" style="color:#444;"><i class="zmdi zmdi-github zmdi-hc-4x"></i></a>
@endif
@foreach($socialDrivers as $driver => $name)
<a id="social-register-{{$driver}}" class="button block muted-light svg text-left" href="{{ baseUrl("/register/service/" . $driver) }}">
@icon($driver)
{{ trans('auth.sign_up_with', ['socialDriver' => $name]) }}
</a>
@endforeach
@endif
</div>
</div>

View File

@ -23,9 +23,10 @@
@include('partials/custom-styles')
<!-- Custom user content -->
@if(setting('app-custom-head'))
@if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
<!-- Custom user content -->
{!! setting('app-custom-head') !!}
<!-- End custom user content -->
@endif
</head>
<body class="@yield('body-class')" ng-app="bookStack">

View File

@ -0,0 +1,80 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $book->name }}</title>
<style>
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/css/export-styles.css')) !!}
@endif
.page-break {
page-break-after: always;
}
.chapter-hint {
color: #888;
margin-top: 32px;
}
.chapter-hint + h1 {
margin-top: 0;
}
ul.contents ul li {
list-style: circle;
}
@media screen {
.page-break {
border-top: 1px solid #DDD;
}
}
</style>
@yield('head')
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-content">
<h1 style="font-size: 4.8em">{{$book->name}}</h1>
<p>{{ $book->description }}</p>
@if(count($bookChildren) > 0)
<ul class="contents">
@foreach($bookChildren as $bookChild)
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
<ul>
@foreach($bookChild->pages as $page)
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
@endforeach
</ul>
@endif
@endforeach
</ul>
@endif
@foreach($bookChildren as $bookChild)
<div class="page-break"></div>
<h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
@if($bookChild->isA('chapter'))
<p>{{ $bookChild->description }}</p>
@if(count($bookChild->pages) > 0)
@foreach($bookChild->pages as $page)
<div class="page-break"></div>
<div class="chapter-hint">{{$bookChild->name}}</div>
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
{!! $page->html !!}
@endforeach
@endif
@else
{!! $bookChild->html !!}
@endif
@endforeach
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -5,11 +5,19 @@
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-6 faded">
<div class="col-sm-6 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
<div class="col-md-6">
<div class="col-sm-6">
<div class="action-buttons faded">
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.pages_export') }}</div>
<ul class="wide">
<li><a href="{{ $book->getUrl('/export/html') }}" target="_blank">{{ trans('entities.pages_export_html') }} <span class="text-muted float right">.html</span></a></li>
<li><a href="{{ $book->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.pages_export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
<li><a href="{{ $book->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.pages_export_text') }} <span class="text-muted float right">.txt</span></a></li>
</ul>
</span>
@if(userCan('page-create', $book))
<a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
@endif

View File

@ -0,0 +1,54 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $chapter->name }}</title>
<style>
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/css/export-styles.css')) !!}
@endif
.page-break {
page-break-after: always;
}
ul.contents ul li {
list-style: circle;
}
@media screen {
.page-break {
border-top: 1px solid #DDD;
}
}
</style>
@yield('head')
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-content">
<h1 style="font-size: 4.8em">{{$chapter->name}}</h1>
<p>{{ $chapter->description }}</p>
@if(count($pages) > 0)
<ul class="contents">
@foreach($pages as $page)
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
@endforeach
</ul>
@endif
@foreach($pages as $page)
<div class="page-break"></div>
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
{!! $page->html !!}
@endforeach
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -10,6 +10,14 @@
</div>
<div class="col-sm-4 faded">
<div class="action-buttons">
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.pages_export') }}</div>
<ul class="wide">
<li><a href="{{ $chapter->getUrl('/export/html') }}" target="_blank">{{ trans('entities.pages_export_html') }} <span class="text-muted float right">.html</span></a></li>
<li><a href="{{ $chapter->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.pages_export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
<li><a href="{{ $chapter->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.pages_export_text') }} <span class="text-muted float right">.txt</span></a></li>
</ul>
</span>
@if(userCan('page-create', $chapter))
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
@endif

View File

@ -5,7 +5,9 @@
<title>{{ $page->name }}</title>
<style>
{!! $css !!}
@if (!app()->environment('testing'))
{!! file_get_contents(public_path('/css/export-styles.css')) !!}
@endif
</style>
@yield('head')
</head>

View File

@ -1,6 +1,6 @@
<div ng-non-bindable>
<h1 id="bkmrk-page-title" class="float left">{{$page->name}}</h1>
<h1 id="bkmrk-page-title">{{$page->name}}</h1>
<div style="clear:left;"></div>

View File

@ -30,11 +30,5 @@
clear: both;
display: block;
}
.tag-display {
min-width: 0;
max-width: none;
display: none;
}
</style>
@stop

View File

@ -30,7 +30,7 @@
<header id="header">
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="col-sm-6">
<a href="{{ baseUrl('/') }}" class="logo">
@if(setting('app-logo', '') !== 'none')
@ -41,7 +41,7 @@
@endif
</a>
</div>
<div class="col-md-6">
<div class="col-sm-6">
<div class="float right">
<div class="links text-center">
@yield('header-buttons')

View File

@ -59,30 +59,18 @@
<h3>{{ trans('settings.users_social_accounts') }}</h3>
<p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
<div class="row">
@if(isset($activeSocialDrivers['google']))
@foreach($activeSocialDrivers as $driver => $enabled)
<div class="col-md-3 text-center">
<div><i class="zmdi zmdi-google-plus-box zmdi-hc-4x" style="color: #DC4E41;"></i></div>
<div>@icon($driver, ['width' => 56])</div>
<div>
@if($user->hasSocialAccount('google'))
<a href="{{ baseUrl("/login/service/google/detach") }}" class="button neg">{{ trans('settings.users_social_disconnect') }}</a>
@if($user->hasSocialAccount($driver))
<a href="{{ baseUrl("/login/service/{$driver}/detach") }}" class="button neg">{{ trans('settings.users_social_disconnect') }}</a>
@else
<a href="{{ baseUrl("/login/service/google") }}" class="button pos">{{ trans('settings.users_social_connect') }}</a>
<a href="{{ baseUrl("/login/service/{$driver}") }}" class="button pos">{{ trans('settings.users_social_connect') }}</a>
@endif
</div>
</div>
@endif
@if(isset($activeSocialDrivers['github']))
<div class="col-md-3 text-center">
<div><i class="zmdi zmdi-github zmdi-hc-4x" style="color: #444;"></i></div>
<div>
@if($user->hasSocialAccount('github'))
<a href="{{ baseUrl("/login/service/github/detach") }}" class="button neg">{{ trans('settings.users_social_disconnect') }}</a>
@else
<a href="{{ baseUrl("/login/service/github") }}" class="button pos">{{ trans('settings.users_social_connect') }}</a>
@endif
</div>
</div>
@endif
@endforeach
</div>
@endif

View File

@ -26,6 +26,9 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
Route::get('/{bookSlug}/export/html', 'BookController@exportHtml');
Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf');
Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText');
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
@ -64,6 +67,9 @@ Route::group(['middleware' => 'auth'], function () {
Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterController@exportPdf');
Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterController@exportHtml');
Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterController@exportPlainText');
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
@ -129,7 +135,7 @@ Route::group(['middleware' => 'auth'], function () {
// Settings
Route::group(['prefix' => 'settings'], function() {
Route::get('/', 'SettingController@index');
Route::get('/', 'SettingController@index')->name('settings');
Route::post('/', 'SettingController@update');
// Users

View File

@ -1,10 +1,7 @@
<?php
<?php namespace Tests;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ActivityTrackingTest extends TestCase
class ActivityTrackingTest extends BrowserKitTest
{
public function test_recently_viewed_books()

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class AttachmentTest extends TestCase
class AttachmentTest extends BrowserKitTest
{
/**
* Get a test file that can be uploaded
@ -75,7 +75,6 @@ class AttachmentTest extends TestCase
{
$page = \BookStack\Page::first();
$this->asAdmin();
$admin = $this->getAdmin();
$fileName = 'upload_test_file.txt';
$this->uploadFile($fileName, $page->id);

View File

@ -1,9 +1,9 @@
<?php
<?php namespace Tests;
use BookStack\Notifications\ConfirmEmail;
use Illuminate\Support\Facades\Notification;
class AuthTest extends TestCase
class AuthTest extends BrowserKitTest
{
public function test_auth_working()
@ -88,7 +88,7 @@ class AuthTest extends TestCase
->press('Resend Confirmation Email');
// Get confirmation and confirm notification matches
$emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
$emailConfirmation = \DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) {
return $notification->token === $emailConfirmation->token;
});
@ -177,7 +177,7 @@ class AuthTest extends TestCase
->seePageIs('/settings/users');
$userPassword = \BookStack\User::find($user->id)->password;
$this->assertTrue(Hash::check('newpassword', $userPassword));
$this->assertTrue(\Hash::check('newpassword', $userPassword));
}
public function test_user_deletion()
@ -220,6 +220,9 @@ class AuthTest extends TestCase
public function test_reset_password_flow()
{
Notification::fake();
$this->visit('/login')->click('Forgot Password?')
->seePageIs('/password/email')
->type('admin@admin.com', 'email')
@ -230,8 +233,12 @@ class AuthTest extends TestCase
'email' => 'admin@admin.com'
]);
$reset = DB::table('password_resets')->where('email', '=', 'admin@admin.com')->first();
$this->visit('/password/reset/' . $reset->token)
$user = \BookStack\User::where('email', '=', 'admin@admin.com')->first();
Notification::assertSentTo($user, \BookStack\Notifications\ResetPassword::class);
$n = Notification::sent($user, \BookStack\Notifications\ResetPassword::class);
$this->visit('/password/reset/' . $n->first()->token)
->see('Reset Password')
->submitForm('Reset Password', [
'email' => 'admin@admin.com',

View File

@ -1,9 +1,7 @@
<?php
use BookStack\Services\LdapService;
<?php namespace Tests;
use BookStack\User;
class LdapTest extends \TestCase
class LdapTest extends BrowserKitTest
{
protected $mockLdap;
@ -14,7 +12,7 @@ class LdapTest extends \TestCase
{
parent::setUp();
app('config')->set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']);
$this->mockLdap = Mockery::mock(BookStack\Services\Ldap::class);
$this->mockLdap = \Mockery::mock(\BookStack\Services\Ldap::class);
$this->app['BookStack\Services\Ldap'] = $this->mockLdap;
$this->mockUser = factory(User::class)->make();
}
@ -24,7 +22,7 @@ class LdapTest extends \TestCase
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->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],
@ -52,7 +50,7 @@ class LdapTest extends \TestCase
$this->mockLdap->shouldReceive('setVersion')->once();
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'cn' => [$this->mockUser->name],
'dn' => $ldapDn,
@ -75,7 +73,7 @@ class LdapTest extends \TestCase
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->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],

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class SocialAuthTest extends TestCase
class SocialAuthTest extends BrowserKitTest
{
public function test_social_registration()
@ -11,10 +11,10 @@ class SocialAuthTest extends TestCase
$this->setSettings(['registration-enabled' => 'true']);
config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
$mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
$mockSocialite = \Mockery::mock('Laravel\Socialite\Contracts\Factory');
$this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
$mockSocialDriver = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$mockSocialUser = Mockery::mock('\Laravel\Socialite\Contracts\User');
$mockSocialDriver = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
$mockSocialUser = \Mockery::mock('\Laravel\Socialite\Contracts\User');
$mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver);
$mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/'));
@ -34,18 +34,16 @@ class SocialAuthTest extends TestCase
public function test_social_login()
{
$user = factory(\BookStack\User::class)->make();
config([
'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
'APP_URL' => 'http://localhost'
]);
$mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
$mockSocialite = \Mockery::mock('Laravel\Socialite\Contracts\Factory');
$this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
$mockSocialDriver = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$mockSocialUser = Mockery::mock('\Laravel\Socialite\Contracts\User');
$mockSocialDriver = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
$mockSocialUser = \Mockery::mock('\Laravel\Socialite\Contracts\User');
$mockSocialUser->shouldReceive('getId')->twice()->andReturn('logintest123');
@ -68,7 +66,7 @@ class SocialAuthTest extends TestCase
->seePageIs('/login');
// Test social callback with matching social account
DB::table('social_accounts')->insert([
\DB::table('social_accounts')->insert([
'user_id' => $this->getAdmin()->id,
'driver' => 'github',
'driver_id' => 'logintest123'

235
tests/BrowserKitTest.php Normal file
View File

@ -0,0 +1,235 @@
<?php namespace Tests;
use BookStack\Role;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase;
use Symfony\Component\DomCrawler\Crawler;
abstract class BrowserKitTest extends TestCase
{
use DatabaseTransactions;
/**
* The base URL to use while testing the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
// Local user instances
private $admin;
private $editor;
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
/**
* Set the current user context to be an admin.
* @return $this
*/
public function asAdmin()
{
return $this->actingAs($this->getAdmin());
}
/**
* Get the current admin user.
* @return mixed
*/
public function getAdmin() {
if($this->admin === null) {
$adminRole = Role::getSystemRole('admin');
$this->admin = $adminRole->users->first();
}
return $this->admin;
}
/**
* Set the current editor context to be an editor.
* @return $this
*/
public function asEditor()
{
if ($this->editor === null) {
$this->editor = $this->getEditor();
}
return $this->actingAs($this->editor);
}
/**
* Get a user that's not a system user such as the guest user.
*/
public function getNormalUser()
{
return \BookStack\User::where('system_name', '=', null)->get()->last();
}
/**
* Quickly sets an array of settings.
* @param $settingsArray
*/
protected function setSettings($settingsArray)
{
$settings = app('BookStack\Services\SettingService');
foreach ($settingsArray as $key => $value) {
$settings->put($key, $value);
}
}
/**
* Create a group of entities that belong to a specific user.
* @param $creatorUser
* @param $updaterUser
* @return array
*/
protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
{
if ($updaterUser === false) $updaterUser = $creatorUser;
$book = factory(\BookStack\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
$chapter = factory(\BookStack\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
$page = factory(\BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
$book->chapters()->saveMany([$chapter]);
$chapter->pages()->saveMany([$page]);
$restrictionService = $this->app[\BookStack\Services\PermissionService::class];
$restrictionService->buildJointPermissionsForEntity($book);
return [
'book' => $book,
'chapter' => $chapter,
'page' => $page
];
}
/**
* Quick way to create a new user
* @param array $attributes
* @return mixed
*/
protected function getEditor($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
$role = Role::getRole('editor');
$user->attachRole($role);;
return $user;
}
/**
* Quick way to create a new user without any permissions
* @param array $attributes
* @return mixed
*/
protected function getNewBlankUser($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
return $user;
}
/**
* Assert that a given string is seen inside an element.
*
* @param bool|string|null $element
* @param integer $position
* @param string $text
* @param bool $negate
* @return $this
*/
protected function seeInNthElement($element, $position, $text, $negate = false)
{
$method = $negate ? 'assertNotRegExp' : 'assertRegExp';
$rawPattern = preg_quote($text, '/');
$escapedPattern = preg_quote(e($text), '/');
$content = $this->crawler->filter($element)->eq($position)->html();
$pattern = $rawPattern == $escapedPattern
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
$this->$method("/$pattern/i", $content);
return $this;
}
/**
* Assert that the current page matches a given URI.
*
* @param string $uri
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->assertEquals(
$uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
);
return $this;
}
/**
* Do a forced visit that does not error out on exception.
* @param string $uri
* @param array $parameters
* @param array $cookies
* @param array $files
* @return $this
*/
protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
{
$method = 'GET';
$uri = $this->prepareUrlForRequest($uri);
$this->call($method, $uri, $parameters, $cookies, $files);
$this->clearInputs()->followRedirects();
$this->currentUri = $this->app->make('request')->fullUrl();
$this->crawler = new Crawler($this->response->getContent(), $uri);
return $this;
}
/**
* Click the text within the selected element.
* @param $parentElement
* @param $linkText
* @return $this
*/
protected function clickInElement($parentElement, $linkText)
{
$elem = $this->crawler->filter($parentElement);
$link = $elem->selectLink($linkText);
$this->visit($link->link()->getUri());
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
*/
protected function pageHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
*/
protected function pageNotHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
return $this;
}
}

102
tests/CommandsTest.php Normal file
View File

@ -0,0 +1,102 @@
<?php namespace Tests;
use BookStack\JointPermission;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class CommandsTest extends TestCase
{
public function test_clear_views_command()
{
$this->asEditor();
$page = Page::first();
$this->get($page->getUrl());
$this->assertDatabaseHas('views', [
'user_id' => $this->getEditor()->id,
'viewable_id' => $page->id,
'views' => 1
]);
$exitCode = \Artisan::call('bookstack:clear-views');
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('views', [
'user_id' => $this->getEditor()->id
]);
}
public function test_clear_activity_command()
{
$this->asEditor();
$page = Page::first();
\Activity::add($page, 'page_update', $page->book->id);
$this->assertDatabaseHas('activities', [
'key' => 'page_update',
'entity_id' => $page->id,
'user_id' => $this->getEditor()->id
]);
$exitCode = \Artisan::call('bookstack:clear-activity');
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('activities', [
'key' => 'page_update'
]);
}
public function test_clear_revisions_command()
{
$this->asEditor();
$entityRepo = $this->app[EntityRepo::class];
$page = Page::first();
$entityRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
$entityRepo->updatePageDraft($page, ['name' => 'updated page', 'html' => '<p>new content in draft</p>', 'summary' => 'page revision testing']);
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
'type' => 'version'
]);
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
'type' => 'update_draft'
]);
$exitCode = \Artisan::call('bookstack:clear-revisions');
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('page_revisions', [
'page_id' => $page->id,
'type' => 'version'
]);
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
'type' => 'update_draft'
]);
$exitCode = \Artisan::call('bookstack:clear-revisions', ['--all' => true]);
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('page_revisions', [
'page_id' => $page->id,
'type' => 'update_draft'
]);
}
public function test_regen_permissions_command()
{
JointPermission::query()->truncate();
$page = Page::first();
$this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]);
$exitCode = \Artisan::call('bookstack:regenerate-permissions');
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseHas('joint_permissions', ['entity_id' => $page->id]);
}
}

View File

@ -0,0 +1,18 @@
<?php namespace Tests;
use Illuminate\Contracts\Console\Kernel;
trait CreatesApplication
{
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
}

View File

@ -1,8 +1,6 @@
<?php
<?php namespace Tests;
use Illuminate\Support\Facades\DB;
class EntitySearchTest extends TestCase
class EntitySearchTest extends BrowserKitTest
{
public function test_page_search()

View File

@ -1,8 +1,6 @@
<?php
<?php namespace Tests;
use Illuminate\Support\Facades\DB;
class EntityTest extends TestCase
class EntityTest extends BrowserKitTest
{
public function test_entity_creation()

115
tests/Entity/ExportTest.php Normal file
View File

@ -0,0 +1,115 @@
<?php namespace Tests;
use BookStack\Chapter;
use BookStack\Page;
class ExportTest extends TestCase
{
public function test_page_text_export()
{
$page = Page::first();
$this->asEditor();
$resp = $this->get($page->getUrl('/export/plaintext'));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt');
}
public function test_page_pdf_export()
{
$page = Page::first();
$this->asEditor();
$resp = $this->get($page->getUrl('/export/pdf'));
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf');
}
public function test_page_html_export()
{
$page = Page::first();
$this->asEditor();
$resp = $this->get($page->getUrl('/export/html'));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html');
}
public function test_book_text_export()
{
$page = Page::first();
$book = $page->book;
$this->asEditor();
$resp = $this->get($book->getUrl('/export/plaintext'));
$resp->assertStatus(200);
$resp->assertSee($book->name);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt');
}
public function test_book_pdf_export()
{
$page = Page::first();
$book = $page->book;
$this->asEditor();
$resp = $this->get($book->getUrl('/export/pdf'));
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf');
}
public function test_book_html_export()
{
$page = Page::first();
$book = $page->book;
$this->asEditor();
$resp = $this->get($book->getUrl('/export/html'));
$resp->assertStatus(200);
$resp->assertSee($book->name);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html');
}
public function test_chapter_text_export()
{
$chapter = Chapter::first();
$page = $chapter->pages[0];
$this->asEditor();
$resp = $this->get($chapter->getUrl('/export/plaintext'));
$resp->assertStatus(200);
$resp->assertSee($chapter->name);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt');
}
public function test_chapter_pdf_export()
{
$chapter = Chapter::first();
$this->asEditor();
$resp = $this->get($chapter->getUrl('/export/pdf'));
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf');
}
public function test_chapter_html_export()
{
$chapter = Chapter::first();
$page = $chapter->pages[0];
$this->asEditor();
$resp = $this->get($chapter->getUrl('/export/html'));
$resp->assertStatus(200);
$resp->assertSee($chapter->name);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html');
}
}

View File

@ -1,7 +1,6 @@
<?php
<?php namespace Tests;
class MarkdownTest extends TestCase
class MarkdownTest extends BrowserKitTest
{
protected $page;

View File

@ -1,33 +1,56 @@
<?php
<?php namespace Tests;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class PageContentTest extends TestCase
{
public function test_page_includes()
{
$page = \BookStack\Page::first();
$secondPage = \BookStack\Page::all()->get(2);
$page = Page::first();
$secondPage = Page::all()->get(2);
$secondPage->html = "<p id='section1'>Hello, This is a test</p><p id='section2'>This is a second block of content</p>";
$secondPage->save();
$this->asAdmin()->visit($page->getUrl())
->dontSee('Hello, This is a test');
$this->asEditor();
$pageContent = $this->get($page->getUrl());
$pageContent->assertDontSee('Hello, This is a test');
$originalHtml = $page->html;
$page->html .= "{{@{$secondPage->id}}}";
$page->save();
$this->asAdmin()->visit($page->getUrl())
->see('Hello, This is a test')
->see('This is a second block of content');
$pageContent = $this->get($page->getUrl());
$pageContent->assertSee('Hello, This is a test');
$pageContent->assertSee('This is a second block of content');
$page->html = $originalHtml . " Well {{@{$secondPage->id}#section2}}";
$page->save();
$this->asAdmin()->visit($page->getUrl())
->dontSee('Hello, This is a test')
->see('Well This is a second block of content');
$pageContent = $this->get($page->getUrl());
$pageContent->assertDontSee('Hello, This is a test');
$pageContent->assertSee('Well This is a second block of content');
}
public function test_page_revision_views_viewable()
{
$this->asEditor();
$entityRepo = $this->app[EntityRepo::class];
$page = Page::first();
$entityRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
$pageRevision = $page->revisions->last();
$revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
$revisionView->assertStatus(200);
$revisionView->assertSee('new content');
$revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id . '/changes');
$revisionView->assertStatus(200);
$revisionView->assertSee('new content');
}
}

View File

@ -1,7 +1,7 @@
<?php
<?php namespace Tests;
class PageDraftTest extends TestCase
class PageDraftTest extends BrowserKitTest
{
protected $page;
protected $entityRepo;

View File

@ -1,4 +1,8 @@
<?php
<?php namespace Tests;
use BookStack\Book;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class SortTest extends TestCase
{
@ -13,13 +17,14 @@ class SortTest extends TestCase
public function test_drafts_do_not_show_up()
{
$this->asAdmin();
$entityRepo = app('\BookStack\Repos\EntityRepo');
$entityRepo = app(EntityRepo::class);
$draft = $entityRepo->getDraftPage($this->book);
$this->visit($this->book->getUrl())
->see($draft->name)
->visit($this->book->getUrl() . '/sort')
->dontSee($draft->name);
$resp = $this->get($this->book->getUrl());
$resp->assertSee($draft->name);
$resp = $this->get($this->book->getUrl() . '/sort');
$resp->assertDontSee($draft->name);
}
public function test_page_move()
@ -27,17 +32,21 @@ class SortTest extends TestCase
$page = \BookStack\Page::first();
$currentBook = $page->book;
$newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
$this->asAdmin()->visit($page->getUrl() . '/move')
->see('Move Page')
->type('book:' . $newBook->id, 'entity_selection')->press('Move Page');
$resp = $this->asAdmin()->get($page->getUrl() . '/move');
$resp->assertSee('Move Page');
$movePageResp = $this->put($page->getUrl() . '/move', [
'entity_selection' => 'book:' . $newBook->id
]);
$page = \BookStack\Page::find($page->id);
$this->seePageIs($page->getUrl());
$movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
$this->visit($newBook->getUrl())
->seeInNthElement('.activity-list-item', 0, 'moved page')
->seeInNthElement('.activity-list-item', 0, $page->name);
$newBookResp = $this->get($newBook->getUrl());
$newBookResp->assertSee('moved page');
$newBookResp->assertSee($page->name);
}
public function test_chapter_move()
@ -47,22 +56,68 @@ class SortTest extends TestCase
$pageToCheck = $chapter->pages->first();
$newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
$this->asAdmin()->visit($chapter->getUrl() . '/move')
->see('Move Chapter')
->type('book:' . $newBook->id, 'entity_selection')->press('Move Chapter');
$chapterMoveResp = $this->asAdmin()->get($chapter->getUrl() . '/move');
$chapterMoveResp->assertSee('Move Chapter');
$moveChapterResp = $this->put($chapter->getUrl() . '/move', [
'entity_selection' => 'book:' . $newBook->id
]);
$chapter = \BookStack\Chapter::find($chapter->id);
$this->seePageIs($chapter->getUrl());
$moveChapterResp->assertRedirect($chapter->getUrl());
$this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');
$this->visit($newBook->getUrl())
->seeInNthElement('.activity-list-item', 0, 'moved chapter')
->seeInNthElement('.activity-list-item', 0, $chapter->name);
$newBookResp = $this->get($newBook->getUrl());
$newBookResp->assertSee('moved chapter');
$newBookResp->assertSee($chapter->name);
$pageToCheck = \BookStack\Page::find($pageToCheck->id);
$this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book');
$this->visit($pageToCheck->getUrl())
->see($newBook->name);
$pageCheckResp = $this->get($pageToCheck->getUrl());
$pageCheckResp->assertSee($newBook->name);
}
public function test_book_sort()
{
$oldBook = Book::query()->first();
$chapterToMove = $this->newChapter(['name' => 'chapter to move'], $oldBook);
$newBook = $this->newBook(['name' => 'New sort book']);
$pagesToMove = Page::query()->take(5)->get();
// Create request data
$reqData = [
[
'id' => $chapterToMove->id,
'sort' => 0,
'parentChapter' => false,
'type' => 'chapter',
'book' => $newBook->id
]
];
foreach ($pagesToMove as $index => $page) {
$reqData[] = [
'id' => $page->id,
'sort' => $index,
'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false,
'type' => 'page',
'book' => $newBook->id
];
}
$sortResp = $this->asAdmin()->put($newBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]);
$sortResp->assertRedirect($newBook->getUrl());
$sortResp->assertStatus(302);
$this->assertDatabaseHas('chapters', [
'id' => $chapterToMove->id,
'book_id' => $newBook->id,
'priority' => 0
]);
$this->assertTrue($newBook->chapters()->count() === 1);
$this->assertTrue($newBook->chapters()->first()->pages()->count() === 1);
$checkPage = $pagesToMove[1];
$checkResp = $this->get(Page::find($checkPage->id)->getUrl());
$checkResp->assertSee($newBook->name);
}
}

View File

@ -1,10 +1,10 @@
<?php namespace Entity;
<?php namespace Tests;
use BookStack\Tag;
use BookStack\Page;
use BookStack\Services\PermissionService;
class TagTest extends \TestCase
class TagTest extends BrowserKitTest
{
protected $defaultTagCount = 20;

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class ImageTest extends TestCase
class ImageTest extends BrowserKitTest
{
/**

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class RestrictionsTest extends TestCase
class RestrictionsTest extends BrowserKitTest
{
protected $user;
protected $viewer;

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class RolesTest extends TestCase
class RolesTest extends BrowserKitTest
{
protected $user;

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class PublicActionTest extends TestCase
class PublicActionTest extends BrowserKitTest
{
public function test_app_not_public()
@ -84,7 +84,7 @@ class PublicActionTest extends TestCase
{
$page = \BookStack\Page::first();
$this->asAdmin()->visit($page->getUrl());
Auth::logout();
\Auth::logout();
view()->share('pageTitle', '');
$this->forceVisit('/cats/dogs/hippos');
$this->dontSee($page->name);

View File

@ -1,37 +1,19 @@
<?php
<?php namespace Tests;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Repos\EntityRepo;
use BookStack\Role;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\DomCrawler\Crawler;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
class TestCase extends Illuminate\Foundation\Testing\TestCase
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use DatabaseTransactions;
/**
* The base URL to use while testing the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
// Local user instances
private $admin;
private $editor;
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
protected $admin;
protected $editor;
/**
* Set the current user context to be an admin.
@ -48,187 +30,50 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
*/
public function getAdmin() {
if($this->admin === null) {
$adminRole = \BookStack\Role::getRole('admin');
$adminRole = Role::getSystemRole('admin');
$this->admin = $adminRole->users->first();
}
return $this->admin;
}
/**
* Set the current editor context to be an editor.
* Set the current user context to be an editor.
* @return $this
*/
public function asEditor()
{
if ($this->editor === null) {
$this->editor = $this->getEditor();
}
return $this->actingAs($this->editor);
return $this->actingAs($this->getEditor());
}
/**
* Get a user that's not a system user such as the guest user.
*/
public function getNormalUser()
{
return \BookStack\User::where('system_name', '=', null)->get()->last();
}
/**
* Quickly sets an array of settings.
* @param $settingsArray
*/
protected function setSettings($settingsArray)
{
$settings = app('BookStack\Services\SettingService');
foreach ($settingsArray as $key => $value) {
$settings->put($key, $value);
}
}
/**
* Create a group of entities that belong to a specific user.
* @param $creatorUser
* @param $updaterUser
* @return array
*/
protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
{
if ($updaterUser === false) $updaterUser = $creatorUser;
$book = factory(BookStack\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
$chapter = factory(BookStack\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
$page = factory(BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
$book->chapters()->saveMany([$chapter]);
$chapter->pages()->saveMany([$page]);
$restrictionService = $this->app[\BookStack\Services\PermissionService::class];
$restrictionService->buildJointPermissionsForEntity($book);
return [
'book' => $book,
'chapter' => $chapter,
'page' => $page
];
}
/**
* Quick way to create a new user
* @param array $attributes
* Get a editor user.
* @return mixed
*/
protected function getEditor($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);;
return $user;
public function getEditor() {
if($this->editor === null) {
$editorRole = Role::getRole('editor');
$this->editor = $editorRole->users->first();
}
return $this->editor;
}
/**
* Quick way to create a new user without any permissions
* @param array $attributes
* @return mixed
* Create and return a new book.
* @param array $input
* @return Book
*/
protected function getNewBlankUser($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
return $user;
public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
return $this->app[EntityRepo::class]->createFromInput('book', $input, false);
}
/**
* Assert that a given string is seen inside an element.
*
* @param bool|string|null $element
* @param integer $position
* @param string $text
* @param bool $negate
* @return $this
* Create and return a new test chapter
* @param array $input
* @param Book $book
* @return Chapter
*/
protected function seeInNthElement($element, $position, $text, $negate = false)
{
$method = $negate ? 'assertNotRegExp' : 'assertRegExp';
$rawPattern = preg_quote($text, '/');
$escapedPattern = preg_quote(e($text), '/');
$content = $this->crawler->filter($element)->eq($position)->html();
$pattern = $rawPattern == $escapedPattern
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
$this->$method("/$pattern/i", $content);
return $this;
}
/**
* Assert that the current page matches a given URI.
*
* @param string $uri
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->assertEquals(
$uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
);
return $this;
}
/**
* Do a forced visit that does not error out on exception.
* @param string $uri
* @param array $parameters
* @param array $cookies
* @param array $files
* @return $this
*/
protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
{
$method = 'GET';
$uri = $this->prepareUrlForRequest($uri);
$this->call($method, $uri, $parameters, $cookies, $files);
$this->clearInputs()->followRedirects();
$this->currentUri = $this->app->make('request')->fullUrl();
$this->crawler = new Crawler($this->response->getContent(), $uri);
return $this;
}
/**
* Click the text within the selected element.
* @param $parentElement
* @param $linkText
* @return $this
*/
protected function clickInElement($parentElement, $linkText)
{
$elem = $this->crawler->filter($parentElement);
$link = $elem->selectLink($linkText);
$this->visit($link->link()->getUri());
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
* @return bool
*/
protected function pageHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
* @return bool
*/
protected function pageNotHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
return $this;
public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
}
}

View File

@ -1,6 +1,6 @@
<?php
<?php namespace Tests;
class UserProfileTest extends TestCase
class UserProfileTest extends BrowserKitTest
{
protected $user;
@ -55,8 +55,8 @@ class UserProfileTest extends TestCase
$newUser = $this->getEditor();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
Activity::add($entities['book'], 'book_update', $entities['book']->id);
Activity::add($entities['page'], 'page_create', $entities['book']->id);
\Activity::add($entities['book'], 'book_update', $entities['book']->id);
\Activity::add($entities['page'], 'page_create', $entities['book']->id);
$this->asAdmin()->visit('/user/' . $newUser->id)
->seeInElement('#recent-activity', 'updated book')
@ -69,8 +69,8 @@ class UserProfileTest extends TestCase
$newUser = $this->getEditor();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
Activity::add($entities['book'], 'book_update', $entities['book']->id);
Activity::add($entities['page'], 'page_create', $entities['book']->id);
\Activity::add($entities['book'], 'book_update', $entities['book']->id);
\Activity::add($entities['page'], 'page_create', $entities['book']->id);
$this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name)
->seePageIs('/user/' . $newUser->id)

View File

@ -1 +1 @@
v0.14.3
v0.15.0