Merge branch 'master' into release

This commit is contained in:
Dan Brown 2016-01-31 18:01:25 +00:00
commit 56ae86646f
57 changed files with 2411 additions and 732 deletions

View File

@ -7,7 +7,7 @@ APP_KEY=SomeRandomString
DB_HOST=localhost
DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database__user_password
DB_PASSWORD=database_user_password
# Cache and session
CACHE_DRIVER=file
@ -25,6 +25,9 @@ STORAGE_S3_BUCKET=false
# Used to prefix image urls for when using custom domains/cdns
STORAGE_URL=false
# General auth
AUTH_METHOD=standard
# Social Authentication information. Defaults as off.
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
@ -33,8 +36,16 @@ GOOGLE_APP_SECRET=false
# URL used for social login redirects, NO TRAILING SLASH
APP_URL=http://bookstack.dev
# External services
USE_GRAVATAR=true
# External services such as Gravatar
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Mail settings
MAIL_DRIVER=smtp

View File

@ -3,8 +3,11 @@
namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
class Handler extends ExceptionHandler
{
@ -14,7 +17,10 @@ class Handler extends ExceptionHandler
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
/**

View File

@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
use Exception;
class ImageUploadException extends Exception {}

View File

@ -0,0 +1,9 @@
<?php namespace BookStack\Exceptions;
use Exception;
class LdapException extends Exception
{
}

View File

@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
@ -29,9 +30,10 @@ class AuthController extends Controller
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $loginPath = '/login';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $username = 'email';
protected $socialAuthService;
protected $emailConfirmationService;
@ -49,6 +51,7 @@ class AuthController extends Controller
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
parent::__construct();
}
@ -105,6 +108,38 @@ class AuthController extends Controller
return $this->registerUser($userData);
}
/**
* Overrides the action when a user is authenticated.
* If the user authenticated but does not exist in the user table we create them.
* @param Request $request
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
*/
protected function authenticated(Request $request, Authenticatable $user)
{
// Explicitly log them out for now if they do no exist.
if (!$user->exists) auth()->logout($user);
if (!$user->exists && $user->email === null && !$request->has('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
$user->email = $request->get('email');
}
if (!$user->exists) {
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
return redirect()->intended($this->redirectPath());
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
@ -156,13 +191,14 @@ class AuthController extends Controller
}
$newUser->email_confirmed = true;
auth()->login($newUser);
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
return redirect($this->redirectPath());
}
/**
* Show the page to tell the user to check thier email
* Show the page to tell the user to check their email
* and confirm their address.
*/
public function getRegisterConfirmation()
@ -222,7 +258,7 @@ class AuthController extends Controller
]);
$user = $this->userRepo->getByEmail($request->get('email'));
$this->emailConfirmationService->sendConfirmation($user);
\Session::flash('success', 'Confirmation email resent, Please check your inbox.');
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
return redirect('/register/confirm');
}
@ -232,13 +268,9 @@ class AuthController extends Controller
*/
public function getLogin()
{
if (view()->exists('auth.authenticate')) {
return view('auth.authenticate');
}
$socialDrivers = $this->socialAuthService->getActiveDrivers();
return view('auth.login', ['socialDrivers' => $socialDrivers]);
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
@ -253,7 +285,7 @@ class AuthController extends Controller
}
/**
* Redirect to the social site for authentication initended to register.
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
* @return mixed
*/

View File

@ -48,7 +48,7 @@ abstract class Controller extends BaseController
*/
protected function preventAccessForDemoUsers()
{
if (env('APP_ENV', 'production') === 'demo') $this->showPermissionError();
if (config('app.env') === 'demo') $this->showPermissionError();
}
/**

View File

@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
@ -69,7 +70,13 @@ class ImageController extends Controller
]);
$imageUpload = $request->file('file');
try {
$image = $this->imageRepo->saveNew($imageUpload, $type);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}

View File

@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -18,18 +19,21 @@ class PageController extends Controller
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $exportService;
/**
* PageController constructor.
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param ExportService $exportService
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
parent::__construct();
}
@ -221,4 +225,57 @@ class PageController extends Controller
Activity::add($page, 'page_restore', $book->id);
return redirect($page->getUrl());
}
/**
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
* https://github.com/barryvdh/laravel-dompdf
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
]);
}
/**
* Export a page to a self-contained HTML file.
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
]);
}
/**
* Export a page to a simple plaintext .txt file.
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPlainText($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
]);
}
}

View File

@ -46,7 +46,8 @@ class UserController extends Controller
public function create()
{
$this->checkPermission('user-create');
return view('users/create');
$authMethod = config('auth.method');
return view('users/create', ['authMethod' => $authMethod]);
}
/**
@ -57,22 +58,35 @@ class UserController extends Controller
public function store(Request $request)
{
$this->checkPermission('user-create');
$this->validate($request, [
$validationRules = [
'name' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:5',
'password-confirm' => 'required|same:password',
'role' => 'required|exists:roles,id'
]);
];
$authMethod = config('auth.method');
if ($authMethod === 'standard') {
$validationRules['password'] = 'required|min:5';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap') {
$validationRules['external_auth_id'] = 'required';
}
$this->validate($request, $validationRules);
$user = $this->user->fill($request->all());
$user->password = bcrypt($request->get('password'));
$user->save();
if ($authMethod === 'standard') {
$user->password = bcrypt($request->get('password'));
} elseif ($authMethod === 'ldap') {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
$user->attachRoleId($request->get('role'));
// Get avatar from gravatar and save
if (!env('DISABLE_EXTERNAL_SERVICES', false)) {
if (!config('services.disable_services')) {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
@ -94,10 +108,12 @@ class UserController extends Controller
return $this->currentUser->id == $id;
});
$authMethod = config('auth.method');
$user = $this->user->findOrFail($id);
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle('User Profile');
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]);
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
}
/**
@ -124,17 +140,24 @@ class UserController extends Controller
]);
$user = $this->user->findOrFail($id);
$user->fill($request->except('password'));
$user->fill($request->all());
// Role updates
if ($this->currentUser->can('user-update') && $request->has('role')) {
$user->attachRoleId($request->get('role'));
}
// Password updates
if ($request->has('password') && $request->get('password') != '') {
$password = $request->get('password');
$user->password = bcrypt($password);
}
// External auth id updates
if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
return redirect('/users');
}

View File

@ -38,6 +38,7 @@ class Authenticate
if(auth()->check() && auth()->user()->email_confirmed == false) {
return redirect()->guest('/register/confirm/awaiting');
}
if ($this->auth->guest() && !Setting::get('app-public')) {
if ($request->ajax()) {
return response('Unauthorized.', 401);

View File

@ -18,11 +18,13 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
Route::post('/{bookSlug}/page', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
@ -45,7 +47,6 @@ Route::group(['middleware' => 'auth'], function () {
});
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');

View File

@ -0,0 +1,31 @@
<?php
namespace BookStack\Providers;
use Auth;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
Auth::provider('ldap', function($app, array $config) {
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
});
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace BookStack\Providers;
use BookStack\Role;
use BookStack\Services\LdapService;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class LdapUserProvider implements UserProvider
{
/**
* The user model.
*
* @var string
*/
protected $model;
/**
* @var LdapService
*/
protected $ldapService;
/**
* LdapUserProvider constructor.
* @param $model
* @param LdapService $ldapService
*/
public function __construct($model, LdapService $ldapService)
{
$this->model = $model;
$this->ldapService = $ldapService;
}
/**
* Create a new instance of the model.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function createModel()
{
$class = '\\' . ltrim($this->model, '\\');
return new $class;
}
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
return $this->createModel()->newQuery()->find($identifier);
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
return $model->newQuery()
->where($model->getAuthIdentifierName(), $identifier)
->where($model->getRememberTokenName(), $token)
->first();
}
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
if ($user->exists) {
$user->setRememberToken($token);
$user->save();
}
}
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get user via LDAP
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
if ($userDetails === null) return null;
// Search current user base by looking up a uid
$model = $this->createModel();
$currentUser = $model->newQuery()
->where('external_auth_id', $userDetails['uid'])
->first();
if ($currentUser !== null) return $currentUser;
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];
$model->email = $userDetails['email'];
$model->email_confirmed = true;
return $model;
}
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
}
}

View File

@ -3,6 +3,7 @@
use BookStack\Role;
use BookStack\User;
use Setting;
class UserRepo
{
@ -47,6 +48,14 @@ class UserRepo
{
$user = $this->create($data);
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
}
return $user;
}
@ -56,7 +65,7 @@ class UserRepo
*/
public function attachDefaultRole($user)
{
$roleId = \Setting::get('registration-role');
$roleId = Setting::get('registration-role');
if ($roleId === false) $roleId = $this->role->getDefault()->id;
$user->attachRoleId($roleId);
}
@ -87,7 +96,7 @@ class UserRepo
*/
public function create(array $data)
{
return $this->user->create([
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password'])

View File

@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* Sets the default role name for newly registed users.
* Sets the default role name for newly registered users.
* @var string
*/
protected static $default = 'viewer';

View File

@ -0,0 +1,115 @@
<?php namespace BookStack\Services;
use BookStack\Page;
class ExportService
{
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page
* @return mixed|string
*/
public function pageToContainedHtml(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
return $this->containHtml($pageHtml);
}
/**
* 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, 'css' => $cssContent])->render();
$containedHtml = $this->containHtml($pageHtml);
$pdf = \PDF::loadHTML($containedHtml);
return $pdf->output();
}
/**
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @return mixed|string
*/
protected function containHtml($htmlContent)
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$pathString = public_path($srcString);
} else {
$pathString = $srcString;
}
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
}
}
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
// Replace image src with base64 encoded image strings
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
}
}
}
// Replace any relative links with system domain
return $htmlContent;
}
/**
* Converts the page contents into simple plain text.
* This method filters any bad looking content to
* provide a nice final output.
* @param Page $page
* @return mixed
*/
public function pageToPlainText(Page $page)
{
$text = $page->text;
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.
$text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
$text = html_entity_decode($text);
// Add title
$text = $page->name . "\n\n" . $text;
return $text;
}
}

View File

@ -1,5 +1,6 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Intervention\Image\ImageManager;
@ -71,6 +72,7 @@ class ImageService
* @param string $imageData
* @param string $type
* @return Image
* @throws ImageUploadException
*/
private function saveNew($imageName, $imageData, $type)
{
@ -86,17 +88,24 @@ class ImageService
}
$fullPath = $imagePath . $imageName;
if(!is_writable(dirname(public_path($fullPath)))) throw new ImageUploadException('Image Directory ' . public_path($fullPath) . ' is not writable by the server.');
$storage->put($fullPath, $imageData);
$userId = auth()->user()->id;
$image = Image::forceCreate([
$imageDetails = [
'name' => $imageName,
'path' => $fullPath,
'url' => $this->getPublicUrl($fullPath),
'type' => $type,
'created_by' => $userId,
'updated_by' => $userId
]);
'type' => $type
];
if (auth()->user() && auth()->user()->id !== 0) {
$userId = auth()->user()->id;
$imageDetails['created_by'] = $userId;
$imageDetails['updated_by'] = $userId;
}
$image = Image::forceCreate($imageDetails);
return $image;
}
@ -188,6 +197,7 @@ class ImageService
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
$image = $this->saveNewFromUrl($url, 'user', $imageName);
$image->created_by = $user->id;
$image->updated_by = $user->id;
$image->save();
return $image;
}
@ -200,7 +210,7 @@ class ImageService
{
if ($this->storageInstance !== null) return $this->storageInstance;
$storageType = env('STORAGE_TYPE');
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
@ -226,10 +236,10 @@ class ImageService
private function getPublicUrl($filePath)
{
if ($this->storageUrl === null) {
$storageUrl = env('STORAGE_URL');
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
if ($storageUrl == false && env('STORAGE_TYPE') === 's3') {
if ($storageUrl == false && config('filesystems.default') === 's3') {
$storageDetails = config('filesystems.disks.s3');
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}

86
app/Services/Ldap.php Normal file
View File

@ -0,0 +1,86 @@
<?php namespace BookStack\Services;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
* @package BookStack\Services
*/
class Ldap
{
/**
* Connect to a LDAP server.
* @param string $hostName
* @param int $port
* @return resource
*/
public function connect($hostName, $port)
{
return ldap_connect($hostName, $port);
}
/**
* Set the value of a LDAP option for the given connection.
* @param resource $ldapConnection
* @param int $option
* @param mixed $value
* @return bool
*/
public function setOption($ldapConnection, $option, $value)
{
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Search LDAP tree using the provided filter.
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
* @return resource
*/
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
/**
* Get entries from an ldap search result.
* @param resource $ldapConnection
* @param resource $ldapSearchResult
* @return array
*/
public function getEntries($ldapConnection, $ldapSearchResult)
{
return ldap_get_entries($ldapConnection, $ldapSearchResult);
}
/**
* Search and get entries immediately.
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
* @return resource
*/
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
return $this->getEntries($ldapConnection, $search);
}
/**
* Bind to LDAP directory.
* @param resource $ldapConnection
* @param string $bindRdn
* @param string $bindPassword
* @return bool
*/
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}

View File

@ -0,0 +1,148 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
/**
* Class LdapService
* Handles any app-specific LDAP tasks.
* @package BookStack\Services
*/
class LdapService
{
protected $ldap;
protected $ldapConnection;
protected $config;
/**
* LdapService constructor.
* @param Ldap $ldap
*/
public function __construct(Ldap $ldap)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
* @param $userName
* @return array|null
* @throws LdapException
*/
public function getUserDetails($userName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// 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']);
if ($users['count'] === 0) return null;
$user = $users[0];
return [
'uid' => $user['uid'][0],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
];
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
public function validateUserCredentials(Authenticatable $user, $username, $password)
{
$ldapUser = $this->getUserDetails($username);
if ($ldapUser === null) return false;
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
$ldapConnection = $this->getConnection();
try {
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
} catch (\ErrorException $e) {
$ldapBind = false;
}
return $ldapBind;
}
/**
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
* @param $connection
* @throws LdapException
*/
protected function bindSystemUser($connection)
{
$ldapDn = $this->config['dn'];
$ldapPass = $this->config['pass'];
$isAnonymous = ($ldapDn === false || $ldapPass === false);
if ($isAnonymous) {
$ldapBind = $this->ldap->bind($connection);
} else {
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
}
/**
* Get the connection to the LDAP server.
* Creates a new connection if one does not exist.
* @return resource
* @throws LdapException
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) return $this->ldapConnection;
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException('LDAP PHP extension not installed');
}
// Get port from server string if specified.
$ldapServer = explode(':', $this->config['server']);
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
if ($ldapConnection === false) {
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
}
// Set any required options
if ($this->config['version']) {
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
}
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
* @param array $attrs
* @return string
*/
protected function buildFilter($filterString, array $attrs)
{
$newAttrs = [];
foreach ($attrs as $key => $attrText) {
$newKey = '${' . $key . '}';
$newAttrs[$newKey] = $attrText;
}
return strtr($filterString, $newAttrs);
}
}

View File

@ -50,12 +50,16 @@ class SettingService
*/
protected function getValueFromStore($key, $default)
{
$overrideValue = $this->getOverrideValue($key);
if ($overrideValue !== null) return $overrideValue;
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$settingObject = $this->getSettingObjectByKey($key);
if ($settingObject !== null) {
$value = $settingObject->value;
$this->cache->forever($cacheKey, $value);
@ -65,6 +69,10 @@ class SettingService
return $default;
}
/**
* Clear an item from the cache completely.
* @param $key
*/
protected function clearFromCache($key)
{
$cacheKey = $this->cachePrefix . $key;
@ -136,9 +144,23 @@ class SettingService
* @param $key
* @return mixed
*/
private function getSettingObjectByKey($key)
protected function getSettingObjectByKey($key)
{
return $this->setting->where('setting_key', '=', $key)->first();
}
/**
* Returns an override value for a setting based on certain app conditions.
* Used where certain configuration options overrule others.
* Returns null if no override value is available.
* @param $key
* @return bool|null
*/
protected function getOverrideValue($key)
{
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
return null;
}
}

View File

@ -129,7 +129,7 @@ class SocialAuthService
// When a user is logged in, A social account exists but the users do not match.
// Change the user that the social account is assigned to.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used buy another user.');
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
return redirect($currentUser->getEditUrl());
}
@ -172,9 +172,10 @@ class SocialAuthService
*/
private function checkDriverConfigured($driver)
{
$upperName = strtoupper($driver);
$config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)];
return (!in_array(false, $config) && !in_array(null, $config));
$lowerName = strtolower($driver);
$configPrefix = 'services.' . $lowerName . '.';
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
return !in_array(false, $config) && !in_array(null, $config);
}
/**

View File

@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*
* @var array
*/
protected $fillable = ['name', 'email', 'password', 'image_id'];
protected $fillable = ['name', 'email', 'image_id'];
/**
* The attributes excluded from the model's JSON form.
@ -68,7 +68,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}
/**
* Loads the user's permissions from thier role.
* Loads the user's permissions from their role.
*/
private function loadPermissions()
{

View File

@ -6,18 +6,21 @@
"type": "project",
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.1.*",
"laravel/framework": "5.2.*",
"intervention/image": "^2.3",
"laravel/socialite": "^2.0",
"barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.0",
"league/flysystem-aws-s3-v3": "^1.0"
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "0.6.*"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1"
"phpspec/phpspec": "~2.1",
"symfony/dom-crawler": "~3.0",
"symfony/css-selector": "~3.0"
},
"autoload": {
"classmap": [

1172
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,9 @@
return [
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
@ -113,13 +116,11 @@ return [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Routing\ControllerServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
@ -142,6 +143,7 @@ return [
* Third Party
*/
Intervention\Image\ImageServiceProvider::class,
Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class,
@ -149,6 +151,7 @@ return [
/*
* Application Service Providers...
*/
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
@ -208,6 +211,7 @@ return [
*/
'ImageTool' => Intervention\Image\Facades\Image::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
/**

View File

@ -2,66 +2,109 @@
return [
'method' => env('AUTH_METHOD', 'standard'),
/*
|--------------------------------------------------------------------------
| Default Authentication Driver
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the authentication driver that will be utilized.
| This driver manages the retrieval and authentication of the users
| attempting to get access to protected areas of your application.
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'driver' => 'eloquent',
/*
|--------------------------------------------------------------------------
| Authentication Model
|--------------------------------------------------------------------------
|
| When using the "Eloquent" authentication driver, we need to know which
| Eloquent model should be used to retrieve your users. Of course, it
| is often just the "User" model but you may use whatever you like.
|
*/
'providers' => [
'users' => [
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
'model' => BookStack\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Authentication Table
|--------------------------------------------------------------------------
|
| When using the "Database" authentication driver, we need to know which
| table should be used to retrieve your users. We have chosen a basic
| default value but you may easily change it to any table you like.
|
*/
'table' => 'users',
/*
|--------------------------------------------------------------------------
| Password Reset Settings
| Resetting Passwords
|--------------------------------------------------------------------------
|
| Here you may set the options for resetting passwords including the view
| that is your password reset e-mail. You can also set the name of the
| that is your password reset e-mail. You may also set the name of the
| table that maintains all of the reset tokens for your application.
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'password' => [
'passwords' => [
'users' => [
'provider' => 'users',
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
],
],
];

266
config/dompdf.php Normal file
View File

@ -0,0 +1,266 @@
<?php
return array(
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => array(
/**
* The location of the DOMPDF font directory
*
* The location of the directory where DOMPDF will store fonts and font metrics
* Note: This directory must exist and be writable by the webserver process.
* *Please note the trailing slash.*
*
* Notes regarding fonts:
* Additional .afm font metrics can be added by executing load_font.php from command line.
*
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
* increase file size unless font subsetting is enabled. Before embedding a font please
* review your rights under the font license.
*
* Any font specification in the source HTML is translated to the closest font available
* in the font directory.
*
* The pdf standard "Base 14 fonts" are:
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
*
* This directory contains the cached font metrics for the fonts used by DOMPDF.
* This directory can be the same as DOMPDF_FONT_DIR
*
* Note: This directory must exist and be writable by the webserver process.
*/
"DOMPDF_FONT_CACHE" => storage_path('fonts/'),
/**
* The location of a temporary directory.
*
* The directory specified must be writeable by the webserver process.
* The temporary directory is required to download remote images and when
* using the PFDLib back end.
*/
"DOMPDF_TEMP_DIR" => sys_get_temp_dir(),
/**
* ==== IMPORTANT ====
*
* dompdf's "chroot": Prevents dompdf from accessing system files or other
* files on the webserver. All local files opened by dompdf must be in a
* subdirectory of this directory. DO NOT set it to '/' since this could
* allow an attacker to use dompdf to read any files on the server. This
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
"DOMPDF_CHROOT" => realpath(base_path()),
/**
* Whether to use Unicode fonts or not.
*
* When set to true the PDF backend must be set to "CPDF" and fonts must be
* loaded via load_font.php.
*
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
* document must be present in your fonts, however.
*/
"DOMPDF_UNICODE_ENABLED" => true,
/**
* Whether to enable font subsetting or not.
*/
"DOMPDF_ENABLE_FONTSUBSETTING" => false,
/**
* The PDF rendering backend to use
*
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
* Canvas_Factory} ultimately determines which rendering class to instantiate
* based on this setting.
*
* Both PDFLib & CPDF rendering backends provide sufficient rendering
* capabilities for dompdf, however additional features (e.g. object,
* image and font support, etc.) differ between backends. Please see
* {@link PDFLib_Adapter} for more information on the PDFLib backend
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
* on CPDF. Also see the documentation for each backend at the links
* below.
*
* The GD rendering backend is a little different than PDFLib and
* CPDF. Several features of CPDF and PDFLib are not supported or do
* not make any sense when creating image files. For example,
* multiple pages are not supported, nor are PDF 'objects'. Have a
* look at {@link GD_Adapter} for more information. GD support is
* experimental, so use it at your own risk.
*
* @link http://www.pdflib.com
* @link http://www.ros.co.nz/pdf
* @link http://www.php.net/image
*/
"DOMPDF_PDF_BACKEND" => "CPDF",
/**
* PDFlib license key
*
* If you are using a licensed, commercial version of PDFlib, specify
* your license key here. If you are using PDFlib-Lite or are evaluating
* the commercial version of PDFlib, comment out this setting.
*
* @link http://www.pdflib.com
*
* If pdflib present in web server and auto or selected explicitely above,
* a real license code must exist!
*/
//"DOMPDF_PDFLIB_LICENSE" => "your license key here",
/**
* html target media view which should be rendered into pdf.
* List of types and parsing rules for future extensions:
* http://www.w3.org/TR/REC-html40/types.html
* screen, tty, tv, projection, handheld, print, braille, aural, all
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
* Note, even though the generated pdf file is intended for print output,
* 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",
/**
* The default paper size.
*
* North America standard is "letter"; other countries generally "a4"
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
"DOMPDF_DEFAULT_PAPER_SIZE" => "a4",
/**
* The default font family
*
* Used if no suitable fonts can be found. This must exist in the font folder.
* @var string
*/
"DOMPDF_DEFAULT_FONT" => "dejavu sans",
/**
* Image DPI setting
*
* This setting determines the default DPI setting for images and fonts. The
* DPI may be overridden for inline images by explictly setting the
* image's width & height style attributes (i.e. if the image's native
* width is 600 pixels and you specify the image's width as 72 points,
* the image will have a DPI of 600 in the rendered PDF. The DPI of
* background images can not be overridden and is controlled entirely
* via this parameter.
*
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
* If a size in html is given as px (or without unit as image size),
* this tells the corresponding size in pt.
* This adjusts the relative sizes to be similar to the rendering of the
* html page in a reference browser.
*
* In pdf, always 1 pt = 1/72 inch
*
* Rendering resolution of various browsers in px per inch:
* Windows Firefox and Internet Explorer:
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
* Linux Firefox:
* about:config *resolution: Default:96
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
*
* Take care about extra font/image zoom factor of browser.
*
* In images, <img> size in pixel attribute, img css style, are overriding
* the real image dimension in px for rendering.
*
* @var int
*/
"DOMPDF_DPI" => 96,
/**
* Enable inline PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate
* inline PHP contained within <script type="text/php"> ... </script> tags.
*
* Enabling this for documents you do not trust (e.g. arbitrary remote html
* pages) is a security risk. Set this option to false if you wish to process
* untrusted documents.
*
* @var bool
*/
"DOMPDF_ENABLE_PHP" => false,
/**
* Enable inline Javascript
*
* If this setting is set to true then DOMPDF will automatically insert
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
*
* @var bool
*/
"DOMPDF_ENABLE_JAVASCRIPT" => true,
/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
* This is required for part of test case www/test/image_variants.html through www/examples.php
*
* Attention!
* This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
* allowing remote access to dompdf.php or on allowing remote html code to be passed to
* $dompdf = new DOMPDF(, $dompdf->load_html(...,
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* @var bool
*/
"DOMPDF_ENABLE_REMOTE" => true,
/**
* A ratio applied to the fonts height to be more like browsers' line height
*/
"DOMPDF_FONT_HEIGHT_RATIO" => 1.1,
/**
* Enable CSS float
*
* Allows people to disabled CSS float support
* @var bool
*/
"DOMPDF_ENABLE_CSS_FLOAT" => true,
/**
* Use the more-than-experimental HTML5 Lib parser
*/
"DOMPDF_ENABLE_HTML5PARSER" => true,
),
);

View File

@ -15,7 +15,18 @@ return [
|
*/
'default' => 'local',
'default' => env('STORAGE_TYPE', 'local'),
/*
|--------------------------------------------------------------------------
| Storage URL
|--------------------------------------------------------------------------
|
| This is the url to where the storage is located for when using an external
| file storage service, such as s3, to store publicly accessible assets.
|
*/
'url' => env('STORAGE_URL', false),
/*
|--------------------------------------------------------------------------

View File

@ -13,6 +13,8 @@ return [
| to have a conventional place to find your various credentials.
|
*/
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
'callback_url' => env('APP_URL', false),
'mailgun' => [
'domain' => '',
@ -47,4 +49,13 @@ return [
'redirect' => env('APP_URL') . '/login/service/google/callback',
],
'ldap' => [
'server' => env('LDAP_SERVER', false),
'dn' => env('LDAP_DN', false),
'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)
]
];

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddExternalAuthToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('external_auth_id')->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('external_auth_id');
});
}
}

View File

@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) {
elixir(function(mix) {
mix.sass('styles.scss')
.sass('print-styles.scss')
.sass('export-styles.scss')
.browserify('global.js', 'public/js/common.js')
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
});

View File

@ -26,6 +26,7 @@
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_CONNECTION" value="mysql_testing"/>
<env name="MAIL_PRETEND" value="true"/>
<env name="AUTH_METHOD" value="standard"/>
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
</php>
</phpunit>

View File

@ -2,12 +2,24 @@
A platform to create documentation/wiki content. General information about BookStack can be found at https://www.bookstackapp.com/
1. [Requirements](#requirements)
2. [Installation](#installation)
- [Server Rewrite Rules](#url-rewrite-rules)
3. [Updating](#updating-bookstack)
4. [Social Authentication](#social-authentication)
- [Google](#google)
- [GitHub](#github)
5. [LDAP Authentication](#ldap-authentication)
6. [Testing](#testing)
7. [License](#license)
8. [Attribution](#attribution)
## Requirements
BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing.
* PHP >= 5.5.9
* PHP >= 5.5.9, Will need to be usable from the command line.
* OpenSSL PHP Extension
* PDO PHP Extension
* MBstring PHP Extension
@ -21,7 +33,7 @@ BookStack has similar requirements to Laravel. On top of those are some front-en
## Installation
Ensure the requirements are met before installing.
Ensure the above requirements are met before installing. Currently BookStack requires its own domain/subdomain and will not work in a site subdirectory.
This project currently uses the `release` branch of this repository as a stable channel for providing updates.
@ -38,7 +50,7 @@ git clone https://github.com/ssddanbrown/BookStack.git --branch release --single
4. Ensure the `storage` & `bootstrap/cache` folders are writable by the web server.
5. In the application root, Run `php artisan key:generate` to generate a unique application key.
6. If not using apache or if `.htaccess` files are disabled you will have to create some URL rewrite rules as shown below.
7. Run `php migrate` to update the database.
7. Run `php artisan migrate` to update the database.
8. Done! You can now login using the default admin details `admin@admin.com` with a password of `password`. It is recommended to change these details directly after first logging in.
#### URL Rewrite rules
@ -59,6 +71,78 @@ location / {
try_files $uri $uri/ /index.php?$query_string;
}
```
## Updating BookStack
To update BookStack you can run the following command in the root directory of the application:
```
git pull origin release && composer install && php artisan migrate
```
This command will update the repository that was created in the installation, install the PHP dependencies using `composer` then run the database migrations.
## Social Authentication
BookStack currently supports login via both Google and GitHub. Once enabled options for these services will show up in the login, registration and user profile pages. By default these services are disabled. To enable them you will have to create an application on the external services to obtain the require application id's and secrets. Here are instructions to do this for the current supported services:
### Google
1. Open the [Google Developers Console](https://console.developers.google.com/).
2. Create a new project (May have to wait a short while for it to be created).
3. Select 'Enable and manage APIs'.
4. Enable the 'Google+ API'.
5. In 'Credentials' choose the 'OAuth consent screen' tab and enter a product name ('BookStack' or your custom set name).
6. Back in the 'Credentials' tab click 'New credentials' > 'OAuth client ID'.
7. Choose an application type of 'Web application' and enter the following urls under 'Authorized redirect URIs', changing `https://example.com` to your own domain where BookStack is hosted:
- `https://example.com/login/service/google/callback`
- `https://example.com/register/service/google/callback`
8. Click 'Create' and your app_id and secret will be displayed. Replace the false value on both the `GOOGLE_APP_ID` & `GOOGLE_APP_SECRET` variables in the '.env' file in the BookStack root directory with your own app_id and secret.
9. Set the 'APP_URL' environment variable to be the same domain as you entered in step 7. So, in this example, it will be `https://example.com`.
10. All done! Users should now be able to link to their social accounts in their account profile pages and also register/login using their Google accounts.
### Github
1. While logged in, open up your [GitHub developer applications](https://github.com/settings/developers).
2. Click 'Register new application'.
3. Enter an application name ('BookStack' or your custom set name), A link to your app instance under 'Homepage URL' and an 'Authorization callback URL' of the url that your BookStack instance is hosted on then click 'Register application'.
4. A 'Client ID' and a 'Client Secret' value will be shown. Add these two values to the to the `GITHUB_APP_ID` and `GITHUB_APP_SECRET` variables, replacing the default false value, in the '.env' file found in the BookStack root folder.
5. Set the 'APP_URL' environment variable to be the same domain as you entered in step 3.
6. All done! Users should now be able to link to their social accounts in their account profile pages and also register/login using their Github account.
## LDAP Authentication
BookStack can be configured to allow LDAP based user login. While LDAP login is enabled you cannot log in with the standard user/password login and new user registration is disabled. BookStack will only use the LDAP server for getting user details and for authentication. Data on the LDAP server is not currently editable through BookStack.
When a LDAP user logs into BookStack for the first time their BookStack profile will be created and they will be given the default role set under the 'Default user role after registration' option in the application settings.
To set up LDAP-based authentication add or modify the following variables in your `.env` file:
```
# General auth
AUTH_METHOD=ldap
# The LDAP host, Adding a port is optional
LDAP_SERVER=ldap://example.com:389
# The base DN from where users will be searched within.
LDAP_BASE_DN=ou=People,dc=example,dc=com
# The full DN and password of the user used to search the server
# Can both be left as false to bind anonymously
LDAP_DN=false
LDAP_PASS=false
# A filter to use when searching for users
# The user-provided user-name used to replace any occurrences of '${user}'
LDAP_USER_FILTER=(&(uid=${user}))
# Set the LDAP version to use when connecting to the server.
LDAP_VERSION=false
```
You will also need to have the php-ldap extension installed on your system. It's recommended to change your `APP_DEBUG` variable to `true` while setting up LDAP to make any errors visible. Remember to change this back after LDAP is functioning.
A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user uid changes it can be updated in BookStack by an admin by changing the 'External Authentication ID' field on the user's profile.
You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID.
## Testing
@ -71,7 +155,7 @@ php artisan migrate --database=mysql_testing
php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
```
Once done you can run `phpunit` in the application root directory to run all tests.
Once done you can run `phpunit` (or `./vendor/bin/phpunit` if `phpunit` is not found) in the application root directory to run all tests.
## License

View File

@ -56,6 +56,7 @@ module.exports = function (ngApp) {
var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
scope.image = scope.currentImage;
scope.value = scope.currentImage || '';
if (usingIds) scope.value = scope.currentId;
function setImage(imageModel, imageUrl) {
scope.image = imageUrl;

View File

@ -0,0 +1,96 @@
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bold-webfont.eot');
src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
url('/fonts/roboto-bold-webfont.woff') format('woff'),
url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bolditalic-webfont.eot');
src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-italic-webfont.eot');
src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
url('/fonts/roboto-italic-webfont.woff') format('woff'),
url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-light-webfont.eot');
src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-light-webfont.woff2') format('woff2'),
url('/fonts/roboto-light-webfont.woff') format('woff'),
url('/fonts/roboto-light-webfont.ttf') format('truetype'),
url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-lightitalic-webfont.eot');
src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-medium-webfont.eot');
src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
url('/fonts/roboto-medium-webfont.woff') format('woff'),
url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-mediumitalic-webfont.eot');
src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular-webfont.eot');
src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
url('/fonts/roboto-regular-webfont.woff') format('woff'),
url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
font-weight: normal;
font-style: normal;
}

View File

@ -161,6 +161,12 @@ form.search-box {
padding: $-xs 0;
color: #555;
text-align: left !important;
&.wide {
min-width: 220px;
}
.text-muted {
color: #999;
}
a {
display: block;
padding: $-xs $-m;
@ -187,7 +193,7 @@ form.search-box {
}
.faded {
a, button, span {
a, button, span, span > div {
color: #666;
}
.text-button {

View File

@ -27,8 +27,8 @@ $-xs: 6px;
$-xxs: 3px;
// Fonts
$heading: 'Roboto', Helvetica, Arial, sans-serif;
$text: 'Roboto', Helvetica, Arial, sans-serif;
$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
$fs-m: 15px;
$fs-s: 14px;
@ -52,101 +52,3 @@ $text-light: #EEE;
$bs-light: 0 0 4px 1px #CCC;
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bold-webfont.eot');
src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
url('/fonts/roboto-bold-webfont.woff') format('woff'),
url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-bolditalic-webfont.eot');
src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-italic-webfont.eot');
src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
url('/fonts/roboto-italic-webfont.woff') format('woff'),
url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-light-webfont.eot');
src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-light-webfont.woff2') format('woff2'),
url('/fonts/roboto-light-webfont.woff') format('woff'),
url('/fonts/roboto-light-webfont.ttf') format('truetype'),
url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-lightitalic-webfont.eot');
src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-medium-webfont.eot');
src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
url('/fonts/roboto-medium-webfont.woff') format('woff'),
url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-mediumitalic-webfont.eot');
src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular-webfont.eot');
src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
url('/fonts/roboto-regular-webfont.woff') format('woff'),
url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,12 @@
//@import "reset";
@import "variables";
@import "mixins";
@import "html";
@import "text";
@import "grid";
@import "blocks";
@import "forms";
@import "tables";
@import "header";
@import "lists";
@import "pages";

View File

@ -1,5 +1,6 @@
@import "reset";
@import "variables";
@import "fonts";
@import "mixins";
@import "html";
@import "text";

View File

@ -0,0 +1,19 @@
<div class="form-group">
<label for="username">Username</label>
@include('form/text', ['name' => 'username', 'tabindex' => 1])
</div>
@if(session('request-email', false) === true)
<div class="form-group">
<label for="email">Email</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1])
<span class="text-neg">
Please enter an email to use for this account.
</span>
</div>
@endif
<div class="form-group">
<label for="password">Password</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2])
</div>

View File

@ -0,0 +1,10 @@
<div class="form-group">
<label for="email">Email</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1])
</div>
<div class="form-group">
<label for="password">Password</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2])
<span class="block small"><a href="/password/email">Forgot Password?</a></span>
</div>

View File

@ -15,16 +15,8 @@
<form action="/login" method="POST" id="login-form">
{!! csrf_field() !!}
<div class="form-group">
<label for="email">Email</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1])
</div>
<div class="form-group">
<label for="password">Password</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2])
<span class="block small"><a href="/password/email">Forgot Password?</a></span>
</div>
@include('auth/forms/login/' . $authMethod)
<div class="form-group">
<label for="remember" class="inline">Remember Me</label>
@ -34,7 +26,7 @@
<div class="from-group">
<button class="button block pos" tabindex="3">Sign In</button>
<button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> Sign In</button>
</div>
</form>

View File

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $page->name }}</title>
<style>
{!! $css !!}
</style>
@yield('head')
</head>
<body>
<div class="container" id="page-show">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-content">
@include('pages/page-display')
<hr>
<p class="text-muted small">
Created {{$page->created_at->toDayDateTimeString()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
<br>
Last Updated {{$page->updated_at->toDayDateTimeString()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif
</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
@extends('pages/export')
@section('head')
<style>
body {
font-size: 15px;
line-height: 1;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1;
}
table {
max-width: 800px !important;
font-size: 0.8em;
width: auto !important;
}
table td {
width: auto !important;
}
</style>
@stop

View File

@ -19,6 +19,14 @@
</div>
<div class="col-sm-6 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>Export</div>
<ul class="wide">
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li>
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li>
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li>
</ul>
</span>
@if($currentUser->can('page-update'))
<a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
<a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>

View File

@ -5,19 +5,19 @@
<!-- Meta -->
<meta name="viewport" content="width=device-width">
<meta name="token" content="{{ csrf_token() }}">
<meta charset="utf-8">
<!-- Styles and Fonts -->
<link rel="stylesheet" href="{{ versioned_asset('css/styles.css') }}">
<link rel="stylesheet" media="print" href="{{ versioned_asset('css/print-styles.css') }}">
<link href='//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/libs/material-design-iconic-font/css/material-design-iconic-font.min.css">
<!-- Scripts -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="/libs/jquery/jquery.min.js?version=2.1.4"></script>
</head>
<body class="@yield('body-class')" id="app">
<body class="@yield('body-class')" ng-app="bookStack">
@include('partials/notifications')
@ -37,12 +37,15 @@
@yield('header-buttons')
</div>
@if(isset($signedIn) && $signedIn)
<div class="dropdown-container" dropdown>
<span class="user-name" dropdown-toggle>
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
<div class="dropdown-container" data-dropdown>
<span class="user-name" data-dropdown-toggle>
{{ $currentUser->name }} <i class="zmdi zmdi-caret-down"></i>
<span class="name" ng-non-bindable>{{ $currentUser->name }}</span> <i class="zmdi zmdi-caret-down"></i>
</span>
<ul>
<li>
<a href="/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-lg"></i>Edit Profile</a>
</li>
<li>
<a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-lg"></i>Logout</a>
</li>

View File

@ -8,7 +8,7 @@
<form action="/users/create" method="post">
{!! csrf_field() !!}
@include('users/form')
@include('users.forms.' . $authMethod)
</form>
</div>

View File

@ -25,7 +25,7 @@
<h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
{!! csrf_field() !!}
<input type="hidden" name="_method" value="put">
@include('users/form', ['model' => $user])
@include('users.forms.' . $authMethod, ['model' => $user])
</div>
<div class="col-md-6">

View File

@ -0,0 +1,30 @@
<div class="form-group">
<label for="name">Name</label>
@include('form.text', ['name' => 'name'])
</div>
@if($currentUser->can('user-update'))
<div class="form-group">
<label for="email">Email</label>
@include('form.text', ['name' => 'email'])
</div>
@endif
@if($currentUser->can('user-update'))
<div class="form-group">
<label for="role">User Role</label>
@include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name'])
</div>
@endif
@if($currentUser->can('user-update'))
<div class="form-group">
<label for="external_auth_id">External Authentication ID</label>
@include('form.text', ['name' => 'external_auth_id'])
</div>
@endif
<div class="form-group">
<a href="/users" class="button muted">Cancel</a>
<button class="button pos" type="submit">Save</button>
</div>

View File

@ -1,11 +1,11 @@
<div class="form-group">
<label for="name">Name</label>
@include('form/text', ['name' => 'name'])
@include('form.text', ['name' => 'name'])
</div>
<div class="form-group">
<label for="email">Email</label>
@include('form/text', ['name' => 'email'])
@include('form.text', ['name' => 'email'])
</div>
@if($currentUser->can('user-update'))
@ -25,12 +25,12 @@
<div class="form-group">
<label for="password">Password</label>
@include('form/password', ['name' => 'password'])
@include('form.password', ['name' => 'password'])
</div>
<div class="form-group">
<label for="password-confirm">Confirm Password</label>
@include('form/password', ['name' => 'password-confirm'])
@include('form.password', ['name' => 'password-confirm'])
</div>
<div class="form-group">

2
storage/fonts/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
class ActivityTrackingTest extends TestCase
{
public function testRecentlyViewedBooks()
public function test_recently_viewed_books()
{
$books = \BookStack\Book::all()->take(10);
@ -21,7 +21,7 @@ class ActivityTrackingTest extends TestCase
->seeInElement('#recents', $books[1]->name);
}
public function testPopularBooks()
public function test_popular_books()
{
$books = \BookStack\Book::all()->take(10);

View File

@ -5,23 +5,19 @@ use BookStack\EmailConfirmation;
class AuthTest extends TestCase
{
public function testAuthWorking()
public function test_auth_working()
{
$this->visit('/')
->seePageIs('/login');
}
public function testLogin()
public function test_login()
{
$this->visit('/')
->seePageIs('/login');
$this->login('admin@admin.com', 'password')
->seePageIs('/')
->see('BookStack');
->seePageIs('/');
}
public function testPublicViewing()
public function test_public_viewing()
{
$settings = app('BookStack\Services\SettingService');
$settings->put('app-public', 'true');
@ -30,7 +26,7 @@ class AuthTest extends TestCase
->see('Sign In');
}
public function testRegistrationShowing()
public function test_registration_showing()
{
// Ensure registration form is showing
$this->setSettings(['registration-enabled' => 'true']);
@ -40,7 +36,7 @@ class AuthTest extends TestCase
->seePageIs('/register');
}
public function testNormalRegistration()
public function test_normal_registration()
{
// Set settings and get user instance
$this->setSettings(['registration-enabled' => 'true']);
@ -58,7 +54,8 @@ class AuthTest extends TestCase
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
}
public function testConfirmedRegistration()
public function test_confirmed_registration()
{
// Set settings and get user instance
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
@ -102,7 +99,32 @@ class AuthTest extends TestCase
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
}
public function testUserCreation()
public function test_restricted_registration()
{
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
$user = factory(\BookStack\User::class)->make();
// Go through registration process
$this->visit('/register')
->type($user->name, '#name')
->type($user->email, '#email')
->type($user->password, '#password')
->press('Create Account')
->seePageIs('/register')
->dontSeeInDatabase('users', ['email' => $user->email])
->see('That email domain does not have access to this application');
$user->email = 'barry@example.com';
$this->visit('/register')
->type($user->name, '#name')
->type($user->email, '#email')
->type($user->password, '#password')
->press('Create Account')
->seePageIs('/register/confirm')
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
}
public function test_user_creation()
{
$user = factory(\BookStack\User::class)->make();
@ -120,7 +142,7 @@ class AuthTest extends TestCase
->see($user->name);
}
public function testUserUpdating()
public function test_user_updating()
{
$user = \BookStack\User::all()->last();
$password = $user->password;
@ -136,7 +158,7 @@ class AuthTest extends TestCase
->notSeeInDatabase('users', ['name' => $user->name]);
}
public function testUserPasswordUpdate()
public function test_user_password_update()
{
$user = \BookStack\User::all()->last();
$userProfilePage = '/users/' . $user->id;
@ -156,7 +178,7 @@ class AuthTest extends TestCase
$this->assertTrue(Hash::check('newpassword', $userPassword));
}
public function testUserDeletion()
public function test_user_deletion()
{
$userDetails = factory(\BookStack\User::class)->make();
$user = $this->getNewUser($userDetails->toArray());
@ -170,7 +192,7 @@ class AuthTest extends TestCase
->notSeeInDatabase('users', ['name' => $user->name]);
}
public function testUserCannotBeDeletedIfLastAdmin()
public function test_user_cannot_be_deleted_if_last_admin()
{
$adminRole = \BookStack\Role::getRole('admin');
// Ensure we currently only have 1 admin user
@ -184,7 +206,7 @@ class AuthTest extends TestCase
->see('You cannot delete the only admin');
}
public function testLogout()
public function test_logout()
{
$this->asAdmin()
->visit('/')
@ -200,7 +222,7 @@ class AuthTest extends TestCase
* @param string $password
* @return $this
*/
private function login($email, $password)
protected function login($email, $password)
{
return $this->visit('/login')
->type($email, '#email')

110
tests/Auth/LdapTest.php Normal file
View File

@ -0,0 +1,110 @@
<?php
use BookStack\Services\LdapService;
use BookStack\User;
class LdapTest extends \TestCase
{
protected $mockLdap;
protected $mockUser;
protected $resourceId = 'resource-test';
public function setUp()
{
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->app['BookStack\Services\Ldap'] = $this->mockLdap;
$this->mockUser = factory(User::class)->make();
}
public function test_login()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setOption')->once();
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test'.config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Sign In')
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
->press('Sign In')
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
}
public function test_initial_incorrect_details()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setOption')->once();
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test'.config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Sign In')
->seePageIs('/login')->see('These credentials do not match our records.')
->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
}
public function test_create_user_form()
{
$this->asAdmin()->visit('/users/create')
->dontSee('Password')
->type($this->mockUser->name, '#name')
->type($this->mockUser->email, '#email')
->press('Save')
->see('The external auth id field is required.')
->type($this->mockUser->name, '#external_auth_id')
->press('Save')
->seePageIs('/users')
->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
}
public function test_user_edit_form()
{
$editUser = User::all()->last();
$this->asAdmin()->visit('/users/' . $editUser->id)
->see('Edit User')
->dontSee('Password')
->type('test_auth_id', '#external_auth_id')
->press('Save')
->seePageIs('/users')
->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
}
public function test_registration_disabled()
{
$this->visit('/register')
->seePageIs('/login');
}
public function test_non_admins_cannot_change_auth_id()
{
$testUser = User::all()->last();
$this->actingAs($testUser)->visit('/users/' . $testUser->id)
->dontSee('External Authentication');
}
}

View File

@ -3,13 +3,13 @@
class SocialAuthTest extends TestCase
{
public function testSocialRegistration()
public function test_social_registration()
{
// http://docs.mockery.io/en/latest/reference/startup_methods.html
$user = factory(\BookStack\User::class)->make();
$this->setSettings(['registration-enabled' => 'true']);
$this->setEnvironment(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
$mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
$this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
@ -32,11 +32,4 @@ class SocialAuthTest extends TestCase
$this->seeInDatabase('social_accounts', ['user_id' => $user->id]);
}
protected function setEnvironment($array)
{
foreach ($array as $key => $value) {
putenv("$key=$value");
}
}
}

View File

@ -5,7 +5,7 @@ use Illuminate\Support\Facades\DB;
class EntityTest extends TestCase
{
public function testEntityCreation()
public function test_entity_creation()
{
// Test Creation
@ -51,7 +51,7 @@ class EntityTest extends TestCase
return \BookStack\Book::find($book->id);
}
public function testBookSortPageShows()
public function test_book_sort_page_shows()
{
$books = \BookStack\Book::all();
$bookToSort = $books[0];
@ -65,7 +65,7 @@ class EntityTest extends TestCase
->see($books[1]->name);
}
public function testBookSortItemReturnsBookContent()
public function test_book_sort_item_returns_book_content()
{
$books = \BookStack\Book::all();
$bookToSort = $books[0];
@ -155,7 +155,7 @@ class EntityTest extends TestCase
return $book;
}
public function testPageSearch()
public function test_page_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->first();
@ -170,7 +170,7 @@ class EntityTest extends TestCase
->seePageIs($page->getUrl());
}
public function testInvalidPageSearch()
public function test_invalid_page_search()
{
$this->asAdmin()
->visit('/')
@ -180,7 +180,7 @@ class EntityTest extends TestCase
->seeStatusCode(200);
}
public function testEmptySearchRedirectsBack()
public function test_empty_search_redirects_back()
{
$this->asAdmin()
->visit('/')
@ -188,7 +188,7 @@ class EntityTest extends TestCase
->seePageIs('/');
}
public function testBookSearch()
public function test_book_search()
{
$book = \BookStack\Book::all()->first();
$page = $book->pages->last();
@ -202,7 +202,7 @@ class EntityTest extends TestCase
->see($chapter->name);
}
public function testEmptyBookSearchRedirectsBack()
public function test_empty_book_search_redirects_back()
{
$book = \BookStack\Book::all()->first();
$this->asAdmin()
@ -212,7 +212,7 @@ class EntityTest extends TestCase
}
public function testEntitiesViewableAfterCreatorDeletion()
public function test_entities_viewable_after_creator_deletion()
{
// Create required assets and revisions
$creator = $this->getNewUser();
@ -225,7 +225,7 @@ class EntityTest extends TestCase
$this->checkEntitiesViewable($entities);
}
public function testEntitiesViewableAfterUpdaterDeletion()
public function test_entities_viewable_after_updater_deletion()
{
// Create required assets and revisions
$creator = $this->getNewUser();

View File

@ -3,7 +3,7 @@
class PublicViewTest extends TestCase
{
public function testBooksViewable()
public function test_books_viewable()
{
$this->setSettings(['app-public' => 'true']);
$books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get();
@ -13,14 +13,14 @@ class PublicViewTest extends TestCase
$this->visit('/books')
->seeStatusCode(200)
->see($books[0]->name)
// Check indavidual book page is showing and it's child contents are visible.
// Check individual book page is showing and it's child contents are visible.
->click($bookToVisit->name)
->seePageIs($bookToVisit->getUrl())
->see($bookToVisit->name)
->see($bookToVisit->chapters()->first()->name);
}
public function testChaptersViewable()
public function test_chapters_viewable()
{
$this->setSettings(['app-public' => 'true']);
$chapterToVisit = \BookStack\Chapter::first();
@ -30,7 +30,7 @@ class PublicViewTest extends TestCase
$this->visit($chapterToVisit->getUrl())
->seeStatusCode(200)
->see($chapterToVisit->name)
// Check indavidual chapter page is showing and it's child contents are visible.
// Check individual chapter page is showing and it's child contents are visible.
->see($pageToVisit->name)
->click($pageToVisit->name)
->see($chapterToVisit->book->name)