Dan Brown

Merge branch 'master' into release

Showing 57 changed files with 1406 additions and 245 deletions
...@@ -7,7 +7,7 @@ APP_KEY=SomeRandomString ...@@ -7,7 +7,7 @@ APP_KEY=SomeRandomString
7 DB_HOST=localhost 7 DB_HOST=localhost
8 DB_DATABASE=database_database 8 DB_DATABASE=database_database
9 DB_USERNAME=database_username 9 DB_USERNAME=database_username
10 -DB_PASSWORD=database__user_password 10 +DB_PASSWORD=database_user_password
11 11
12 # Cache and session 12 # Cache and session
13 CACHE_DRIVER=file 13 CACHE_DRIVER=file
...@@ -25,6 +25,9 @@ STORAGE_S3_BUCKET=false ...@@ -25,6 +25,9 @@ STORAGE_S3_BUCKET=false
25 # Used to prefix image urls for when using custom domains/cdns 25 # Used to prefix image urls for when using custom domains/cdns
26 STORAGE_URL=false 26 STORAGE_URL=false
27 27
28 +# General auth
29 +AUTH_METHOD=standard
30 +
28 # Social Authentication information. Defaults as off. 31 # Social Authentication information. Defaults as off.
29 GITHUB_APP_ID=false 32 GITHUB_APP_ID=false
30 GITHUB_APP_SECRET=false 33 GITHUB_APP_SECRET=false
...@@ -33,8 +36,16 @@ GOOGLE_APP_SECRET=false ...@@ -33,8 +36,16 @@ GOOGLE_APP_SECRET=false
33 # URL used for social login redirects, NO TRAILING SLASH 36 # URL used for social login redirects, NO TRAILING SLASH
34 APP_URL=http://bookstack.dev 37 APP_URL=http://bookstack.dev
35 38
36 -# External services 39 +# External services such as Gravatar
37 -USE_GRAVATAR=true 40 +DISABLE_EXTERNAL_SERVICES=false
41 +
42 +# LDAP Settings
43 +LDAP_SERVER=false
44 +LDAP_BASE_DN=false
45 +LDAP_DN=false
46 +LDAP_PASS=false
47 +LDAP_USER_FILTER=false
48 +LDAP_VERSION=false
38 49
39 # Mail settings 50 # Mail settings
40 MAIL_DRIVER=smtp 51 MAIL_DRIVER=smtp
......
...@@ -3,8 +3,11 @@ ...@@ -3,8 +3,11 @@
3 namespace BookStack\Exceptions; 3 namespace BookStack\Exceptions;
4 4
5 use Exception; 5 use Exception;
6 +use Illuminate\Contracts\Validation\ValidationException;
7 +use Illuminate\Database\Eloquent\ModelNotFoundException;
6 use Symfony\Component\HttpKernel\Exception\HttpException; 8 use Symfony\Component\HttpKernel\Exception\HttpException;
7 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; 9 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
10 +use Illuminate\Auth\Access\AuthorizationException;
8 11
9 class Handler extends ExceptionHandler 12 class Handler extends ExceptionHandler
10 { 13 {
...@@ -14,7 +17,10 @@ class Handler extends ExceptionHandler ...@@ -14,7 +17,10 @@ class Handler extends ExceptionHandler
14 * @var array 17 * @var array
15 */ 18 */
16 protected $dontReport = [ 19 protected $dontReport = [
20 + AuthorizationException::class,
17 HttpException::class, 21 HttpException::class,
22 + ModelNotFoundException::class,
23 + ValidationException::class,
18 ]; 24 ];
19 25
20 /** 26 /**
......
1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +use Exception;
5 +
6 +class ImageUploadException extends Exception {}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +use Exception;
5 +
6 +class LdapException extends Exception
7 +{
8 +
9 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace BookStack\Http\Controllers\Auth; 3 namespace BookStack\Http\Controllers\Auth;
4 4
5 +use Illuminate\Contracts\Auth\Authenticatable;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 use BookStack\Exceptions\SocialSignInException; 7 use BookStack\Exceptions\SocialSignInException;
7 use BookStack\Exceptions\UserRegistrationException; 8 use BookStack\Exceptions\UserRegistrationException;
...@@ -29,9 +30,10 @@ class AuthController extends Controller ...@@ -29,9 +30,10 @@ class AuthController extends Controller
29 30
30 use AuthenticatesAndRegistersUsers, ThrottlesLogins; 31 use AuthenticatesAndRegistersUsers, ThrottlesLogins;
31 32
32 - protected $loginPath = '/login';
33 protected $redirectPath = '/'; 33 protected $redirectPath = '/';
34 protected $redirectAfterLogout = '/login'; 34 protected $redirectAfterLogout = '/login';
35 + protected $username = 'email';
36 +
35 37
36 protected $socialAuthService; 38 protected $socialAuthService;
37 protected $emailConfirmationService; 39 protected $emailConfirmationService;
...@@ -49,6 +51,7 @@ class AuthController extends Controller ...@@ -49,6 +51,7 @@ class AuthController extends Controller
49 $this->socialAuthService = $socialAuthService; 51 $this->socialAuthService = $socialAuthService;
50 $this->emailConfirmationService = $emailConfirmationService; 52 $this->emailConfirmationService = $emailConfirmationService;
51 $this->userRepo = $userRepo; 53 $this->userRepo = $userRepo;
54 + $this->username = config('auth.method') === 'standard' ? 'email' : 'username';
52 parent::__construct(); 55 parent::__construct();
53 } 56 }
54 57
...@@ -105,6 +108,38 @@ class AuthController extends Controller ...@@ -105,6 +108,38 @@ class AuthController extends Controller
105 return $this->registerUser($userData); 108 return $this->registerUser($userData);
106 } 109 }
107 110
111 +
112 + /**
113 + * Overrides the action when a user is authenticated.
114 + * If the user authenticated but does not exist in the user table we create them.
115 + * @param Request $request
116 + * @param Authenticatable $user
117 + * @return \Illuminate\Http\RedirectResponse
118 + */
119 + protected function authenticated(Request $request, Authenticatable $user)
120 + {
121 + // Explicitly log them out for now if they do no exist.
122 + if (!$user->exists) auth()->logout($user);
123 +
124 + if (!$user->exists && $user->email === null && !$request->has('email')) {
125 + $request->flash();
126 + session()->flash('request-email', true);
127 + return redirect('/login');
128 + }
129 +
130 + if (!$user->exists && $user->email === null && $request->has('email')) {
131 + $user->email = $request->get('email');
132 + }
133 +
134 + if (!$user->exists) {
135 + $user->save();
136 + $this->userRepo->attachDefaultRole($user);
137 + auth()->login($user);
138 + }
139 +
140 + return redirect()->intended($this->redirectPath());
141 + }
142 +
108 /** 143 /**
109 * Register a new user after a registration callback. 144 * Register a new user after a registration callback.
110 * @param $socialDriver 145 * @param $socialDriver
...@@ -156,13 +191,14 @@ class AuthController extends Controller ...@@ -156,13 +191,14 @@ class AuthController extends Controller
156 } 191 }
157 192
158 $newUser->email_confirmed = true; 193 $newUser->email_confirmed = true;
194 +
159 auth()->login($newUser); 195 auth()->login($newUser);
160 session()->flash('success', 'Thanks for signing up! You are now registered and signed in.'); 196 session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
161 return redirect($this->redirectPath()); 197 return redirect($this->redirectPath());
162 } 198 }
163 199
164 /** 200 /**
165 - * Show the page to tell the user to check thier email 201 + * Show the page to tell the user to check their email
166 * and confirm their address. 202 * and confirm their address.
167 */ 203 */
168 public function getRegisterConfirmation() 204 public function getRegisterConfirmation()
...@@ -222,7 +258,7 @@ class AuthController extends Controller ...@@ -222,7 +258,7 @@ class AuthController extends Controller
222 ]); 258 ]);
223 $user = $this->userRepo->getByEmail($request->get('email')); 259 $user = $this->userRepo->getByEmail($request->get('email'));
224 $this->emailConfirmationService->sendConfirmation($user); 260 $this->emailConfirmationService->sendConfirmation($user);
225 - \Session::flash('success', 'Confirmation email resent, Please check your inbox.'); 261 + session()->flash('success', 'Confirmation email resent, Please check your inbox.');
226 return redirect('/register/confirm'); 262 return redirect('/register/confirm');
227 } 263 }
228 264
...@@ -232,13 +268,9 @@ class AuthController extends Controller ...@@ -232,13 +268,9 @@ class AuthController extends Controller
232 */ 268 */
233 public function getLogin() 269 public function getLogin()
234 { 270 {
235 -
236 - if (view()->exists('auth.authenticate')) {
237 - return view('auth.authenticate');
238 - }
239 -
240 $socialDrivers = $this->socialAuthService->getActiveDrivers(); 271 $socialDrivers = $this->socialAuthService->getActiveDrivers();
241 - return view('auth.login', ['socialDrivers' => $socialDrivers]); 272 + $authMethod = config('auth.method');
273 + return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
242 } 274 }
243 275
244 /** 276 /**
...@@ -253,7 +285,7 @@ class AuthController extends Controller ...@@ -253,7 +285,7 @@ class AuthController extends Controller
253 } 285 }
254 286
255 /** 287 /**
256 - * Redirect to the social site for authentication initended to register. 288 + * Redirect to the social site for authentication intended to register.
257 * @param $socialDriver 289 * @param $socialDriver
258 * @return mixed 290 * @return mixed
259 */ 291 */
......
...@@ -48,7 +48,7 @@ abstract class Controller extends BaseController ...@@ -48,7 +48,7 @@ abstract class Controller extends BaseController
48 */ 48 */
49 protected function preventAccessForDemoUsers() 49 protected function preventAccessForDemoUsers()
50 { 50 {
51 - if (env('APP_ENV', 'production') === 'demo') $this->showPermissionError(); 51 + if (config('app.env') === 'demo') $this->showPermissionError();
52 } 52 }
53 53
54 /** 54 /**
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 +use BookStack\Exceptions\ImageUploadException;
5 use BookStack\Repos\ImageRepo; 6 use BookStack\Repos\ImageRepo;
6 use Illuminate\Filesystem\Filesystem as File; 7 use Illuminate\Filesystem\Filesystem as File;
7 use Illuminate\Http\Request; 8 use Illuminate\Http\Request;
...@@ -69,7 +70,13 @@ class ImageController extends Controller ...@@ -69,7 +70,13 @@ class ImageController extends Controller
69 ]); 70 ]);
70 71
71 $imageUpload = $request->file('file'); 72 $imageUpload = $request->file('file');
73 +
74 + try {
72 $image = $this->imageRepo->saveNew($imageUpload, $type); 75 $image = $this->imageRepo->saveNew($imageUpload, $type);
76 + } catch (ImageUploadException $e) {
77 + return response($e->getMessage(), 500);
78 + }
79 +
73 return response()->json($image); 80 return response()->json($image);
74 } 81 }
75 82
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 use Activity; 5 use Activity;
6 +use BookStack\Services\ExportService;
6 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
7 8
8 use Illuminate\Support\Facades\Auth; 9 use Illuminate\Support\Facades\Auth;
...@@ -18,18 +19,21 @@ class PageController extends Controller ...@@ -18,18 +19,21 @@ class PageController extends Controller
18 protected $pageRepo; 19 protected $pageRepo;
19 protected $bookRepo; 20 protected $bookRepo;
20 protected $chapterRepo; 21 protected $chapterRepo;
22 + protected $exportService;
21 23
22 /** 24 /**
23 * PageController constructor. 25 * PageController constructor.
24 * @param PageRepo $pageRepo 26 * @param PageRepo $pageRepo
25 * @param BookRepo $bookRepo 27 * @param BookRepo $bookRepo
26 * @param ChapterRepo $chapterRepo 28 * @param ChapterRepo $chapterRepo
29 + * @param ExportService $exportService
27 */ 30 */
28 - public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo) 31 + public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
29 { 32 {
30 $this->pageRepo = $pageRepo; 33 $this->pageRepo = $pageRepo;
31 $this->bookRepo = $bookRepo; 34 $this->bookRepo = $bookRepo;
32 $this->chapterRepo = $chapterRepo; 35 $this->chapterRepo = $chapterRepo;
36 + $this->exportService = $exportService;
33 parent::__construct(); 37 parent::__construct();
34 } 38 }
35 39
...@@ -221,4 +225,57 @@ class PageController extends Controller ...@@ -221,4 +225,57 @@ class PageController extends Controller
221 Activity::add($page, 'page_restore', $book->id); 225 Activity::add($page, 'page_restore', $book->id);
222 return redirect($page->getUrl()); 226 return redirect($page->getUrl());
223 } 227 }
228 +
229 + /**
230 + * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
231 + * https://github.com/barryvdh/laravel-dompdf
232 + * @param $bookSlug
233 + * @param $pageSlug
234 + * @return \Illuminate\Http\Response
235 + */
236 + public function exportPdf($bookSlug, $pageSlug)
237 + {
238 + $book = $this->bookRepo->getBySlug($bookSlug);
239 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
240 + $pdfContent = $this->exportService->pageToPdf($page);
241 + return response()->make($pdfContent, 200, [
242 + 'Content-Type' => 'application/octet-stream',
243 + 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
244 + ]);
245 + }
246 +
247 + /**
248 + * Export a page to a self-contained HTML file.
249 + * @param $bookSlug
250 + * @param $pageSlug
251 + * @return \Illuminate\Http\Response
252 + */
253 + public function exportHtml($bookSlug, $pageSlug)
254 + {
255 + $book = $this->bookRepo->getBySlug($bookSlug);
256 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
257 + $containedHtml = $this->exportService->pageToContainedHtml($page);
258 + return response()->make($containedHtml, 200, [
259 + 'Content-Type' => 'application/octet-stream',
260 + 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
261 + ]);
262 + }
263 +
264 + /**
265 + * Export a page to a simple plaintext .txt file.
266 + * @param $bookSlug
267 + * @param $pageSlug
268 + * @return \Illuminate\Http\Response
269 + */
270 + public function exportPlainText($bookSlug, $pageSlug)
271 + {
272 + $book = $this->bookRepo->getBySlug($bookSlug);
273 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
274 + $containedHtml = $this->exportService->pageToPlainText($page);
275 + return response()->make($containedHtml, 200, [
276 + 'Content-Type' => 'application/octet-stream',
277 + 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
278 + ]);
279 + }
280 +
224 } 281 }
......
...@@ -46,7 +46,8 @@ class UserController extends Controller ...@@ -46,7 +46,8 @@ class UserController extends Controller
46 public function create() 46 public function create()
47 { 47 {
48 $this->checkPermission('user-create'); 48 $this->checkPermission('user-create');
49 - return view('users/create'); 49 + $authMethod = config('auth.method');
50 + return view('users/create', ['authMethod' => $authMethod]);
50 } 51 }
51 52
52 /** 53 /**
...@@ -57,22 +58,35 @@ class UserController extends Controller ...@@ -57,22 +58,35 @@ class UserController extends Controller
57 public function store(Request $request) 58 public function store(Request $request)
58 { 59 {
59 $this->checkPermission('user-create'); 60 $this->checkPermission('user-create');
60 - $this->validate($request, [ 61 + $validationRules = [
61 'name' => 'required', 62 'name' => 'required',
62 'email' => 'required|email|unique:users,email', 63 'email' => 'required|email|unique:users,email',
63 - 'password' => 'required|min:5',
64 - 'password-confirm' => 'required|same:password',
65 'role' => 'required|exists:roles,id' 64 'role' => 'required|exists:roles,id'
66 - ]); 65 + ];
66 +
67 + $authMethod = config('auth.method');
68 + if ($authMethod === 'standard') {
69 + $validationRules['password'] = 'required|min:5';
70 + $validationRules['password-confirm'] = 'required|same:password';
71 + } elseif ($authMethod === 'ldap') {
72 + $validationRules['external_auth_id'] = 'required';
73 + }
74 + $this->validate($request, $validationRules);
75 +
67 76
68 $user = $this->user->fill($request->all()); 77 $user = $this->user->fill($request->all());
78 +
79 + if ($authMethod === 'standard') {
69 $user->password = bcrypt($request->get('password')); 80 $user->password = bcrypt($request->get('password'));
70 - $user->save(); 81 + } elseif ($authMethod === 'ldap') {
82 + $user->external_auth_id = $request->get('external_auth_id');
83 + }
71 84
85 + $user->save();
72 $user->attachRoleId($request->get('role')); 86 $user->attachRoleId($request->get('role'));
73 87
74 // Get avatar from gravatar and save 88 // Get avatar from gravatar and save
75 - if (!env('DISABLE_EXTERNAL_SERVICES', false)) { 89 + if (!config('services.disable_services')) {
76 $avatar = \Images::saveUserGravatar($user); 90 $avatar = \Images::saveUserGravatar($user);
77 $user->avatar()->associate($avatar); 91 $user->avatar()->associate($avatar);
78 $user->save(); 92 $user->save();
...@@ -94,10 +108,12 @@ class UserController extends Controller ...@@ -94,10 +108,12 @@ class UserController extends Controller
94 return $this->currentUser->id == $id; 108 return $this->currentUser->id == $id;
95 }); 109 });
96 110
111 + $authMethod = config('auth.method');
112 +
97 $user = $this->user->findOrFail($id); 113 $user = $this->user->findOrFail($id);
98 $activeSocialDrivers = $socialAuthService->getActiveDrivers(); 114 $activeSocialDrivers = $socialAuthService->getActiveDrivers();
99 $this->setPageTitle('User Profile'); 115 $this->setPageTitle('User Profile');
100 - return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]); 116 + return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
101 } 117 }
102 118
103 /** 119 /**
...@@ -124,17 +140,24 @@ class UserController extends Controller ...@@ -124,17 +140,24 @@ class UserController extends Controller
124 ]); 140 ]);
125 141
126 $user = $this->user->findOrFail($id); 142 $user = $this->user->findOrFail($id);
127 - $user->fill($request->except('password')); 143 + $user->fill($request->all());
128 144
145 + // Role updates
129 if ($this->currentUser->can('user-update') && $request->has('role')) { 146 if ($this->currentUser->can('user-update') && $request->has('role')) {
130 $user->attachRoleId($request->get('role')); 147 $user->attachRoleId($request->get('role'));
131 } 148 }
132 149
150 + // Password updates
133 if ($request->has('password') && $request->get('password') != '') { 151 if ($request->has('password') && $request->get('password') != '') {
134 $password = $request->get('password'); 152 $password = $request->get('password');
135 $user->password = bcrypt($password); 153 $user->password = bcrypt($password);
136 } 154 }
137 155
156 + // External auth id updates
157 + if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
158 + $user->external_auth_id = $request->get('external_auth_id');
159 + }
160 +
138 $user->save(); 161 $user->save();
139 return redirect('/users'); 162 return redirect('/users');
140 } 163 }
......
...@@ -38,6 +38,7 @@ class Authenticate ...@@ -38,6 +38,7 @@ class Authenticate
38 if(auth()->check() && auth()->user()->email_confirmed == false) { 38 if(auth()->check() && auth()->user()->email_confirmed == false) {
39 return redirect()->guest('/register/confirm/awaiting'); 39 return redirect()->guest('/register/confirm/awaiting');
40 } 40 }
41 +
41 if ($this->auth->guest() && !Setting::get('app-public')) { 42 if ($this->auth->guest() && !Setting::get('app-public')) {
42 if ($request->ajax()) { 43 if ($request->ajax()) {
43 return response('Unauthorized.', 401); 44 return response('Unauthorized.', 401);
......
...@@ -18,17 +18,19 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -18,17 +18,19 @@ Route::group(['middleware' => 'auth'], function () {
18 Route::get('/{bookSlug}/sort', 'BookController@sort'); 18 Route::get('/{bookSlug}/sort', 'BookController@sort');
19 Route::put('/{bookSlug}/sort', 'BookController@saveSort'); 19 Route::put('/{bookSlug}/sort', 'BookController@saveSort');
20 20
21 -
22 // Pages 21 // Pages
23 Route::get('/{bookSlug}/page/create', 'PageController@create'); 22 Route::get('/{bookSlug}/page/create', 'PageController@create');
24 Route::post('/{bookSlug}/page', 'PageController@store'); 23 Route::post('/{bookSlug}/page', 'PageController@store');
25 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 24 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
25 + Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
26 + Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
27 + Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
26 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); 28 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
27 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); 29 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
28 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); 30 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
29 Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy'); 31 Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
30 32
31 - //Revisions 33 + // Revisions
32 Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions'); 34 Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
33 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision'); 35 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
34 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision'); 36 Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
...@@ -45,7 +47,6 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -45,7 +47,6 @@ Route::group(['middleware' => 'auth'], function () {
45 47
46 }); 48 });
47 49
48 -
49 // Users 50 // Users
50 Route::get('/users', 'UserController@index'); 51 Route::get('/users', 'UserController@index');
51 Route::get('/users/create', 'UserController@create'); 52 Route::get('/users/create', 'UserController@create');
......
1 +<?php
2 +
3 +namespace BookStack\Providers;
4 +
5 +use Auth;
6 +use Illuminate\Support\ServiceProvider;
7 +
8 +class AuthServiceProvider extends ServiceProvider
9 +{
10 + /**
11 + * Bootstrap the application services.
12 + *
13 + * @return void
14 + */
15 + public function boot()
16 + {
17 + //
18 + }
19 +
20 + /**
21 + * Register the application services.
22 + *
23 + * @return void
24 + */
25 + public function register()
26 + {
27 + Auth::provider('ldap', function($app, array $config) {
28 + return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
29 + });
30 + }
31 +}
1 +<?php
2 +
3 +namespace BookStack\Providers;
4 +
5 +
6 +use BookStack\Role;
7 +use BookStack\Services\LdapService;
8 +use BookStack\User;
9 +use Illuminate\Contracts\Auth\Authenticatable;
10 +use Illuminate\Contracts\Auth\UserProvider;
11 +
12 +class LdapUserProvider implements UserProvider
13 +{
14 +
15 + /**
16 + * The user model.
17 + *
18 + * @var string
19 + */
20 + protected $model;
21 +
22 + /**
23 + * @var LdapService
24 + */
25 + protected $ldapService;
26 +
27 +
28 + /**
29 + * LdapUserProvider constructor.
30 + * @param $model
31 + * @param LdapService $ldapService
32 + */
33 + public function __construct($model, LdapService $ldapService)
34 + {
35 + $this->model = $model;
36 + $this->ldapService = $ldapService;
37 + }
38 +
39 + /**
40 + * Create a new instance of the model.
41 + *
42 + * @return \Illuminate\Database\Eloquent\Model
43 + */
44 + public function createModel()
45 + {
46 + $class = '\\' . ltrim($this->model, '\\');
47 + return new $class;
48 + }
49 +
50 +
51 + /**
52 + * Retrieve a user by their unique identifier.
53 + *
54 + * @param mixed $identifier
55 + * @return \Illuminate\Contracts\Auth\Authenticatable|null
56 + */
57 + public function retrieveById($identifier)
58 + {
59 + return $this->createModel()->newQuery()->find($identifier);
60 + }
61 +
62 + /**
63 + * Retrieve a user by their unique identifier and "remember me" token.
64 + *
65 + * @param mixed $identifier
66 + * @param string $token
67 + * @return \Illuminate\Contracts\Auth\Authenticatable|null
68 + */
69 + public function retrieveByToken($identifier, $token)
70 + {
71 + $model = $this->createModel();
72 +
73 + return $model->newQuery()
74 + ->where($model->getAuthIdentifierName(), $identifier)
75 + ->where($model->getRememberTokenName(), $token)
76 + ->first();
77 + }
78 +
79 +
80 + /**
81 + * Update the "remember me" token for the given user in storage.
82 + *
83 + * @param \Illuminate\Contracts\Auth\Authenticatable $user
84 + * @param string $token
85 + * @return void
86 + */
87 + public function updateRememberToken(Authenticatable $user, $token)
88 + {
89 + if ($user->exists) {
90 + $user->setRememberToken($token);
91 + $user->save();
92 + }
93 + }
94 +
95 + /**
96 + * Retrieve a user by the given credentials.
97 + *
98 + * @param array $credentials
99 + * @return \Illuminate\Contracts\Auth\Authenticatable|null
100 + */
101 + public function retrieveByCredentials(array $credentials)
102 + {
103 + // Get user via LDAP
104 + $userDetails = $this->ldapService->getUserDetails($credentials['username']);
105 + if ($userDetails === null) return null;
106 +
107 + // Search current user base by looking up a uid
108 + $model = $this->createModel();
109 + $currentUser = $model->newQuery()
110 + ->where('external_auth_id', $userDetails['uid'])
111 + ->first();
112 +
113 + if ($currentUser !== null) return $currentUser;
114 +
115 + $model->name = $userDetails['name'];
116 + $model->external_auth_id = $userDetails['uid'];
117 + $model->email = $userDetails['email'];
118 + $model->email_confirmed = true;
119 + return $model;
120 + }
121 +
122 + /**
123 + * Validate a user against the given credentials.
124 + *
125 + * @param \Illuminate\Contracts\Auth\Authenticatable $user
126 + * @param array $credentials
127 + * @return bool
128 + */
129 + public function validateCredentials(Authenticatable $user, array $credentials)
130 + {
131 + return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
132 + }
133 +}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 3
4 use BookStack\Role; 4 use BookStack\Role;
5 use BookStack\User; 5 use BookStack\User;
6 +use Setting;
6 7
7 class UserRepo 8 class UserRepo
8 { 9 {
...@@ -47,6 +48,14 @@ class UserRepo ...@@ -47,6 +48,14 @@ class UserRepo
47 { 48 {
48 $user = $this->create($data); 49 $user = $this->create($data);
49 $this->attachDefaultRole($user); 50 $this->attachDefaultRole($user);
51 +
52 + // Get avatar from gravatar and save
53 + if (!config('services.disable_services')) {
54 + $avatar = \Images::saveUserGravatar($user);
55 + $user->avatar()->associate($avatar);
56 + $user->save();
57 + }
58 +
50 return $user; 59 return $user;
51 } 60 }
52 61
...@@ -56,7 +65,7 @@ class UserRepo ...@@ -56,7 +65,7 @@ class UserRepo
56 */ 65 */
57 public function attachDefaultRole($user) 66 public function attachDefaultRole($user)
58 { 67 {
59 - $roleId = \Setting::get('registration-role'); 68 + $roleId = Setting::get('registration-role');
60 if ($roleId === false) $roleId = $this->role->getDefault()->id; 69 if ($roleId === false) $roleId = $this->role->getDefault()->id;
61 $user->attachRoleId($roleId); 70 $user->attachRoleId($roleId);
62 } 71 }
...@@ -87,7 +96,7 @@ class UserRepo ...@@ -87,7 +96,7 @@ class UserRepo
87 */ 96 */
88 public function create(array $data) 97 public function create(array $data)
89 { 98 {
90 - return $this->user->create([ 99 + return $this->user->forceCreate([
91 'name' => $data['name'], 100 'name' => $data['name'],
92 'email' => $data['email'], 101 'email' => $data['email'],
93 'password' => bcrypt($data['password']) 102 'password' => bcrypt($data['password'])
......
...@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model; ...@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
7 class Role extends Model 7 class Role extends Model
8 { 8 {
9 /** 9 /**
10 - * Sets the default role name for newly registed users. 10 + * Sets the default role name for newly registered users.
11 * @var string 11 * @var string
12 */ 12 */
13 protected static $default = 'viewer'; 13 protected static $default = 'viewer';
......
1 +<?php namespace BookStack\Services;
2 +
3 +
4 +use BookStack\Page;
5 +
6 +class ExportService
7 +{
8 +
9 + /**
10 + * Convert a page to a self-contained HTML file.
11 + * Includes required CSS & image content. Images are base64 encoded into the HTML.
12 + * @param Page $page
13 + * @return mixed|string
14 + */
15 + public function pageToContainedHtml(Page $page)
16 + {
17 + $cssContent = file_get_contents(public_path('/css/export-styles.css'));
18 + $pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
19 + return $this->containHtml($pageHtml);
20 + }
21 +
22 + /**
23 + * Convert a page to a pdf file.
24 + * @param Page $page
25 + * @return mixed|string
26 + */
27 + public function pageToPdf(Page $page)
28 + {
29 + $cssContent = file_get_contents(public_path('/css/export-styles.css'));
30 + $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
31 + $containedHtml = $this->containHtml($pageHtml);
32 + $pdf = \PDF::loadHTML($containedHtml);
33 + return $pdf->output();
34 + }
35 +
36 + /**
37 + * Bundle of the contents of a html file to be self-contained.
38 + * @param $htmlContent
39 + * @return mixed|string
40 + */
41 + protected function containHtml($htmlContent)
42 + {
43 + $imageTagsOutput = [];
44 + preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
45 +
46 + // Replace image src with base64 encoded image strings
47 + if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
48 + foreach ($imageTagsOutput[0] as $index => $imgMatch) {
49 + $oldImgString = $imgMatch;
50 + $srcString = $imageTagsOutput[2][$index];
51 + if (strpos(trim($srcString), 'http') !== 0) {
52 + $pathString = public_path($srcString);
53 + } else {
54 + $pathString = $srcString;
55 + }
56 + $imageContent = file_get_contents($pathString);
57 + $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
58 + $newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
59 + $htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
60 + }
61 + }
62 +
63 + $linksOutput = [];
64 + preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
65 +
66 + // Replace image src with base64 encoded image strings
67 + if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
68 + foreach ($linksOutput[0] as $index => $linkMatch) {
69 + $oldLinkString = $linkMatch;
70 + $srcString = $linksOutput[2][$index];
71 + if (strpos(trim($srcString), 'http') !== 0) {
72 + $newSrcString = url($srcString);
73 + $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
74 + $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
75 + }
76 + }
77 + }
78 +
79 + // Replace any relative links with system domain
80 + return $htmlContent;
81 + }
82 +
83 + /**
84 + * Converts the page contents into simple plain text.
85 + * This method filters any bad looking content to
86 + * provide a nice final output.
87 + * @param Page $page
88 + * @return mixed
89 + */
90 + public function pageToPlainText(Page $page)
91 + {
92 + $text = $page->text;
93 + // Replace multiple spaces with single spaces
94 + $text = preg_replace('/\ {2,}/', ' ', $text);
95 + // Reduce multiple horrid whitespace characters.
96 + $text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
97 + $text = html_entity_decode($text);
98 + // Add title
99 + $text = $page->name . "\n\n" . $text;
100 + return $text;
101 + }
102 +
103 +}
104 +
105 +
106 +
107 +
108 +
109 +
110 +
111 +
112 +
113 +
114 +
115 +
1 <?php namespace BookStack\Services; 1 <?php namespace BookStack\Services;
2 2
3 +use BookStack\Exceptions\ImageUploadException;
3 use BookStack\Image; 4 use BookStack\Image;
4 use BookStack\User; 5 use BookStack\User;
5 use Intervention\Image\ImageManager; 6 use Intervention\Image\ImageManager;
...@@ -71,6 +72,7 @@ class ImageService ...@@ -71,6 +72,7 @@ class ImageService
71 * @param string $imageData 72 * @param string $imageData
72 * @param string $type 73 * @param string $type
73 * @return Image 74 * @return Image
75 + * @throws ImageUploadException
74 */ 76 */
75 private function saveNew($imageName, $imageData, $type) 77 private function saveNew($imageName, $imageData, $type)
76 { 78 {
...@@ -86,17 +88,24 @@ class ImageService ...@@ -86,17 +88,24 @@ class ImageService
86 } 88 }
87 $fullPath = $imagePath . $imageName; 89 $fullPath = $imagePath . $imageName;
88 90
91 + if(!is_writable(dirname(public_path($fullPath)))) throw new ImageUploadException('Image Directory ' . public_path($fullPath) . ' is not writable by the server.');
92 +
89 $storage->put($fullPath, $imageData); 93 $storage->put($fullPath, $imageData);
90 94
91 - $userId = auth()->user()->id; 95 + $imageDetails = [
92 - $image = Image::forceCreate([
93 'name' => $imageName, 96 'name' => $imageName,
94 'path' => $fullPath, 97 'path' => $fullPath,
95 'url' => $this->getPublicUrl($fullPath), 98 'url' => $this->getPublicUrl($fullPath),
96 - 'type' => $type, 99 + 'type' => $type
97 - 'created_by' => $userId, 100 + ];
98 - 'updated_by' => $userId 101 +
99 - ]); 102 + if (auth()->user() && auth()->user()->id !== 0) {
103 + $userId = auth()->user()->id;
104 + $imageDetails['created_by'] = $userId;
105 + $imageDetails['updated_by'] = $userId;
106 + }
107 +
108 + $image = Image::forceCreate($imageDetails);
100 109
101 return $image; 110 return $image;
102 } 111 }
...@@ -188,6 +197,7 @@ class ImageService ...@@ -188,6 +197,7 @@ class ImageService
188 $imageName = str_replace(' ', '-', $user->name . '-gravatar.png'); 197 $imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
189 $image = $this->saveNewFromUrl($url, 'user', $imageName); 198 $image = $this->saveNewFromUrl($url, 'user', $imageName);
190 $image->created_by = $user->id; 199 $image->created_by = $user->id;
200 + $image->updated_by = $user->id;
191 $image->save(); 201 $image->save();
192 return $image; 202 return $image;
193 } 203 }
...@@ -200,7 +210,7 @@ class ImageService ...@@ -200,7 +210,7 @@ class ImageService
200 { 210 {
201 if ($this->storageInstance !== null) return $this->storageInstance; 211 if ($this->storageInstance !== null) return $this->storageInstance;
202 212
203 - $storageType = env('STORAGE_TYPE'); 213 + $storageType = config('filesystems.default');
204 $this->storageInstance = $this->fileSystem->disk($storageType); 214 $this->storageInstance = $this->fileSystem->disk($storageType);
205 215
206 return $this->storageInstance; 216 return $this->storageInstance;
...@@ -226,10 +236,10 @@ class ImageService ...@@ -226,10 +236,10 @@ class ImageService
226 private function getPublicUrl($filePath) 236 private function getPublicUrl($filePath)
227 { 237 {
228 if ($this->storageUrl === null) { 238 if ($this->storageUrl === null) {
229 - $storageUrl = env('STORAGE_URL'); 239 + $storageUrl = config('filesystems.url');
230 240
231 // Get the standard public s3 url if s3 is set as storage type 241 // Get the standard public s3 url if s3 is set as storage type
232 - if ($storageUrl == false && env('STORAGE_TYPE') === 's3') { 242 + if ($storageUrl == false && config('filesystems.default') === 's3') {
233 $storageDetails = config('filesystems.disks.s3'); 243 $storageDetails = config('filesystems.disks.s3');
234 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; 244 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
235 } 245 }
......
1 +<?php namespace BookStack\Services;
2 +
3 +
4 +/**
5 + * Class Ldap
6 + * An object-orientated thin abstraction wrapper for common PHP LDAP functions.
7 + * Allows the standard LDAP functions to be mocked for testing.
8 + * @package BookStack\Services
9 + */
10 +class Ldap
11 +{
12 +
13 + /**
14 + * Connect to a LDAP server.
15 + * @param string $hostName
16 + * @param int $port
17 + * @return resource
18 + */
19 + public function connect($hostName, $port)
20 + {
21 + return ldap_connect($hostName, $port);
22 + }
23 +
24 + /**
25 + * Set the value of a LDAP option for the given connection.
26 + * @param resource $ldapConnection
27 + * @param int $option
28 + * @param mixed $value
29 + * @return bool
30 + */
31 + public function setOption($ldapConnection, $option, $value)
32 + {
33 + return ldap_set_option($ldapConnection, $option, $value);
34 + }
35 +
36 + /**
37 + * Search LDAP tree using the provided filter.
38 + * @param resource $ldapConnection
39 + * @param string $baseDn
40 + * @param string $filter
41 + * @param array|null $attributes
42 + * @return resource
43 + */
44 + public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
45 + {
46 + return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
47 + }
48 +
49 + /**
50 + * Get entries from an ldap search result.
51 + * @param resource $ldapConnection
52 + * @param resource $ldapSearchResult
53 + * @return array
54 + */
55 + public function getEntries($ldapConnection, $ldapSearchResult)
56 + {
57 + return ldap_get_entries($ldapConnection, $ldapSearchResult);
58 + }
59 +
60 + /**
61 + * Search and get entries immediately.
62 + * @param resource $ldapConnection
63 + * @param string $baseDn
64 + * @param string $filter
65 + * @param array|null $attributes
66 + * @return resource
67 + */
68 + public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
69 + {
70 + $search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
71 + return $this->getEntries($ldapConnection, $search);
72 + }
73 +
74 + /**
75 + * Bind to LDAP directory.
76 + * @param resource $ldapConnection
77 + * @param string $bindRdn
78 + * @param string $bindPassword
79 + * @return bool
80 + */
81 + public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
82 + {
83 + return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
84 + }
85 +
86 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php namespace BookStack\Services;
2 +
3 +
4 +use BookStack\Exceptions\LdapException;
5 +use Illuminate\Contracts\Auth\Authenticatable;
6 +
7 +/**
8 + * Class LdapService
9 + * Handles any app-specific LDAP tasks.
10 + * @package BookStack\Services
11 + */
12 +class LdapService
13 +{
14 +
15 + protected $ldap;
16 + protected $ldapConnection;
17 + protected $config;
18 +
19 + /**
20 + * LdapService constructor.
21 + * @param Ldap $ldap
22 + */
23 + public function __construct(Ldap $ldap)
24 + {
25 + $this->ldap = $ldap;
26 + $this->config = config('services.ldap');
27 + }
28 +
29 + /**
30 + * Get the details of a user from LDAP using the given username.
31 + * User found via configurable user filter.
32 + * @param $userName
33 + * @return array|null
34 + * @throws LdapException
35 + */
36 + public function getUserDetails($userName)
37 + {
38 + $ldapConnection = $this->getConnection();
39 + $this->bindSystemUser($ldapConnection);
40 +
41 + // Find user
42 + $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
43 + $baseDn = $this->config['base_dn'];
44 + $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
45 + if ($users['count'] === 0) return null;
46 +
47 + $user = $users[0];
48 + return [
49 + 'uid' => $user['uid'][0],
50 + 'name' => $user['cn'][0],
51 + 'dn' => $user['dn'],
52 + 'email' => (isset($user['mail'])) ? $user['mail'][0] : null
53 + ];
54 + }
55 +
56 + /**
57 + * @param Authenticatable $user
58 + * @param string $username
59 + * @param string $password
60 + * @return bool
61 + * @throws LdapException
62 + */
63 + public function validateUserCredentials(Authenticatable $user, $username, $password)
64 + {
65 + $ldapUser = $this->getUserDetails($username);
66 + if ($ldapUser === null) return false;
67 + if ($ldapUser['uid'] !== $user->external_auth_id) return false;
68 +
69 + $ldapConnection = $this->getConnection();
70 + try {
71 + $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
72 + } catch (\ErrorException $e) {
73 + $ldapBind = false;
74 + }
75 +
76 + return $ldapBind;
77 + }
78 +
79 + /**
80 + * Bind the system user to the LDAP connection using the given credentials
81 + * otherwise anonymous access is attempted.
82 + * @param $connection
83 + * @throws LdapException
84 + */
85 + protected function bindSystemUser($connection)
86 + {
87 + $ldapDn = $this->config['dn'];
88 + $ldapPass = $this->config['pass'];
89 +
90 + $isAnonymous = ($ldapDn === false || $ldapPass === false);
91 + if ($isAnonymous) {
92 + $ldapBind = $this->ldap->bind($connection);
93 + } else {
94 + $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
95 + }
96 +
97 + if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
98 + }
99 +
100 + /**
101 + * Get the connection to the LDAP server.
102 + * Creates a new connection if one does not exist.
103 + * @return resource
104 + * @throws LdapException
105 + */
106 + protected function getConnection()
107 + {
108 + if ($this->ldapConnection !== null) return $this->ldapConnection;
109 +
110 + // Check LDAP extension in installed
111 + if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
112 + throw new LdapException('LDAP PHP extension not installed');
113 + }
114 +
115 + // Get port from server string if specified.
116 + $ldapServer = explode(':', $this->config['server']);
117 + $ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
118 +
119 + if ($ldapConnection === false) {
120 + throw new LdapException('Cannot connect to ldap server, Initial connection failed');
121 + }
122 +
123 + // Set any required options
124 + if ($this->config['version']) {
125 + $this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
126 + }
127 +
128 + $this->ldapConnection = $ldapConnection;
129 + return $this->ldapConnection;
130 + }
131 +
132 + /**
133 + * Build a filter string by injecting common variables.
134 + * @param string $filterString
135 + * @param array $attrs
136 + * @return string
137 + */
138 + protected function buildFilter($filterString, array $attrs)
139 + {
140 + $newAttrs = [];
141 + foreach ($attrs as $key => $attrText) {
142 + $newKey = '${' . $key . '}';
143 + $newAttrs[$newKey] = $attrText;
144 + }
145 + return strtr($filterString, $newAttrs);
146 + }
147 +
148 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -50,13 +50,17 @@ class SettingService ...@@ -50,13 +50,17 @@ class SettingService
50 */ 50 */
51 protected function getValueFromStore($key, $default) 51 protected function getValueFromStore($key, $default)
52 { 52 {
53 + $overrideValue = $this->getOverrideValue($key);
54 + if ($overrideValue !== null) return $overrideValue;
55 +
53 $cacheKey = $this->cachePrefix . $key; 56 $cacheKey = $this->cachePrefix . $key;
54 if ($this->cache->has($cacheKey)) { 57 if ($this->cache->has($cacheKey)) {
55 return $this->cache->get($cacheKey); 58 return $this->cache->get($cacheKey);
56 } 59 }
57 60
58 $settingObject = $this->getSettingObjectByKey($key); 61 $settingObject = $this->getSettingObjectByKey($key);
59 - if($settingObject !== null) { 62 +
63 + if ($settingObject !== null) {
60 $value = $settingObject->value; 64 $value = $settingObject->value;
61 $this->cache->forever($cacheKey, $value); 65 $this->cache->forever($cacheKey, $value);
62 return $value; 66 return $value;
...@@ -65,6 +69,10 @@ class SettingService ...@@ -65,6 +69,10 @@ class SettingService
65 return $default; 69 return $default;
66 } 70 }
67 71
72 + /**
73 + * Clear an item from the cache completely.
74 + * @param $key
75 + */
68 protected function clearFromCache($key) 76 protected function clearFromCache($key)
69 { 77 {
70 $cacheKey = $this->cachePrefix . $key; 78 $cacheKey = $this->cachePrefix . $key;
...@@ -136,9 +144,23 @@ class SettingService ...@@ -136,9 +144,23 @@ class SettingService
136 * @param $key 144 * @param $key
137 * @return mixed 145 * @return mixed
138 */ 146 */
139 - private function getSettingObjectByKey($key) 147 + protected function getSettingObjectByKey($key)
140 { 148 {
141 return $this->setting->where('setting_key', '=', $key)->first(); 149 return $this->setting->where('setting_key', '=', $key)->first();
142 } 150 }
143 151
152 +
153 + /**
154 + * Returns an override value for a setting based on certain app conditions.
155 + * Used where certain configuration options overrule others.
156 + * Returns null if no override value is available.
157 + * @param $key
158 + * @return bool|null
159 + */
160 + protected function getOverrideValue($key)
161 + {
162 + if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
163 + return null;
164 + }
165 +
144 } 166 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -76,9 +76,9 @@ class SocialAuthService ...@@ -76,9 +76,9 @@ class SocialAuthService
76 throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login'); 76 throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
77 } 77 }
78 78
79 - if($this->userRepo->getByEmail($socialUser->getEmail())) { 79 + if ($this->userRepo->getByEmail($socialUser->getEmail())) {
80 $email = $socialUser->getEmail(); 80 $email = $socialUser->getEmail();
81 - throw new UserRegistrationException('The email '. $email.' is already in use. If you already have an account you can connect your ' . $socialDriver .' account from your profile settings.', '/login'); 81 + throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
82 } 82 }
83 83
84 return $socialUser; 84 return $socialUser;
...@@ -129,7 +129,7 @@ class SocialAuthService ...@@ -129,7 +129,7 @@ class SocialAuthService
129 // When a user is logged in, A social account exists but the users do not match. 129 // When a user is logged in, A social account exists but the users do not match.
130 // Change the user that the social account is assigned to. 130 // Change the user that the social account is assigned to.
131 if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { 131 if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
132 - \Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used buy another user.'); 132 + \Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
133 return redirect($currentUser->getEditUrl()); 133 return redirect($currentUser->getEditUrl());
134 } 134 }
135 135
...@@ -172,9 +172,10 @@ class SocialAuthService ...@@ -172,9 +172,10 @@ class SocialAuthService
172 */ 172 */
173 private function checkDriverConfigured($driver) 173 private function checkDriverConfigured($driver)
174 { 174 {
175 - $upperName = strtoupper($driver); 175 + $lowerName = strtolower($driver);
176 - $config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)]; 176 + $configPrefix = 'services.' . $lowerName . '.';
177 - return (!in_array(false, $config) && !in_array(null, $config)); 177 + $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
178 + return !in_array(false, $config) && !in_array(null, $config);
178 } 179 }
179 180
180 /** 181 /**
......
...@@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
24 * 24 *
25 * @var array 25 * @var array
26 */ 26 */
27 - protected $fillable = ['name', 'email', 'password', 'image_id']; 27 + protected $fillable = ['name', 'email', 'image_id'];
28 28
29 /** 29 /**
30 * The attributes excluded from the model's JSON form. 30 * The attributes excluded from the model's JSON form.
...@@ -68,7 +68,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -68,7 +68,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
68 } 68 }
69 69
70 /** 70 /**
71 - * Loads the user's permissions from thier role. 71 + * Loads the user's permissions from their role.
72 */ 72 */
73 private function loadPermissions() 73 private function loadPermissions()
74 { 74 {
......
...@@ -6,18 +6,21 @@ ...@@ -6,18 +6,21 @@
6 "type": "project", 6 "type": "project",
7 "require": { 7 "require": {
8 "php": ">=5.5.9", 8 "php": ">=5.5.9",
9 - "laravel/framework": "5.1.*", 9 + "laravel/framework": "5.2.*",
10 "intervention/image": "^2.3", 10 "intervention/image": "^2.3",
11 "laravel/socialite": "^2.0", 11 "laravel/socialite": "^2.0",
12 "barryvdh/laravel-ide-helper": "^2.1", 12 "barryvdh/laravel-ide-helper": "^2.1",
13 "barryvdh/laravel-debugbar": "^2.0", 13 "barryvdh/laravel-debugbar": "^2.0",
14 - "league/flysystem-aws-s3-v3": "^1.0" 14 + "league/flysystem-aws-s3-v3": "^1.0",
15 + "barryvdh/laravel-dompdf": "0.6.*"
15 }, 16 },
16 "require-dev": { 17 "require-dev": {
17 "fzaninotto/faker": "~1.4", 18 "fzaninotto/faker": "~1.4",
18 "mockery/mockery": "0.9.*", 19 "mockery/mockery": "0.9.*",
19 "phpunit/phpunit": "~4.0", 20 "phpunit/phpunit": "~4.0",
20 - "phpspec/phpspec": "~2.1" 21 + "phpspec/phpspec": "~2.1",
22 + "symfony/dom-crawler": "~3.0",
23 + "symfony/css-selector": "~3.0"
21 }, 24 },
22 "autoload": { 25 "autoload": {
23 "classmap": [ 26 "classmap": [
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
2 2
3 return [ 3 return [
4 4
5 +
6 + 'env' => env('APP_ENV', 'production'),
7 +
5 /* 8 /*
6 |-------------------------------------------------------------------------- 9 |--------------------------------------------------------------------------
7 | Application Debug Mode 10 | Application Debug Mode
...@@ -113,13 +116,11 @@ return [ ...@@ -113,13 +116,11 @@ return [
113 /* 116 /*
114 * Laravel Framework Service Providers... 117 * Laravel Framework Service Providers...
115 */ 118 */
116 - Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
117 Illuminate\Auth\AuthServiceProvider::class, 119 Illuminate\Auth\AuthServiceProvider::class,
118 Illuminate\Broadcasting\BroadcastServiceProvider::class, 120 Illuminate\Broadcasting\BroadcastServiceProvider::class,
119 Illuminate\Bus\BusServiceProvider::class, 121 Illuminate\Bus\BusServiceProvider::class,
120 Illuminate\Cache\CacheServiceProvider::class, 122 Illuminate\Cache\CacheServiceProvider::class,
121 Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, 123 Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
122 - Illuminate\Routing\ControllerServiceProvider::class,
123 Illuminate\Cookie\CookieServiceProvider::class, 124 Illuminate\Cookie\CookieServiceProvider::class,
124 Illuminate\Database\DatabaseServiceProvider::class, 125 Illuminate\Database\DatabaseServiceProvider::class,
125 Illuminate\Encryption\EncryptionServiceProvider::class, 126 Illuminate\Encryption\EncryptionServiceProvider::class,
...@@ -142,6 +143,7 @@ return [ ...@@ -142,6 +143,7 @@ return [
142 * Third Party 143 * Third Party
143 */ 144 */
144 Intervention\Image\ImageServiceProvider::class, 145 Intervention\Image\ImageServiceProvider::class,
146 + Barryvdh\DomPDF\ServiceProvider::class,
145 Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, 147 Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
146 Barryvdh\Debugbar\ServiceProvider::class, 148 Barryvdh\Debugbar\ServiceProvider::class,
147 149
...@@ -149,6 +151,7 @@ return [ ...@@ -149,6 +151,7 @@ return [
149 /* 151 /*
150 * Application Service Providers... 152 * Application Service Providers...
151 */ 153 */
154 + BookStack\Providers\AuthServiceProvider::class,
152 BookStack\Providers\AppServiceProvider::class, 155 BookStack\Providers\AppServiceProvider::class,
153 BookStack\Providers\EventServiceProvider::class, 156 BookStack\Providers\EventServiceProvider::class,
154 BookStack\Providers\RouteServiceProvider::class, 157 BookStack\Providers\RouteServiceProvider::class,
...@@ -208,6 +211,7 @@ return [ ...@@ -208,6 +211,7 @@ return [
208 */ 211 */
209 212
210 'ImageTool' => Intervention\Image\Facades\Image::class, 213 'ImageTool' => Intervention\Image\Facades\Image::class,
214 + 'PDF' => Barryvdh\DomPDF\Facade::class,
211 'Debugbar' => Barryvdh\Debugbar\Facade::class, 215 'Debugbar' => Barryvdh\Debugbar\Facade::class,
212 216
213 /** 217 /**
......
...@@ -2,66 +2,109 @@ ...@@ -2,66 +2,109 @@
2 2
3 return [ 3 return [
4 4
5 +
6 + 'method' => env('AUTH_METHOD', 'standard'),
7 +
5 /* 8 /*
6 |-------------------------------------------------------------------------- 9 |--------------------------------------------------------------------------
7 - | Default Authentication Driver 10 + | Authentication Defaults
8 |-------------------------------------------------------------------------- 11 |--------------------------------------------------------------------------
9 | 12 |
10 - | This option controls the authentication driver that will be utilized. 13 + | This option controls the default authentication "guard" and password
11 - | This driver manages the retrieval and authentication of the users 14 + | reset options for your application. You may change these defaults
12 - | attempting to get access to protected areas of your application. 15 + | as required, but they're a perfect start for most applications.
13 - |
14 - | Supported: "database", "eloquent"
15 | 16 |
16 */ 17 */
17 18
18 - 'driver' => 'eloquent', 19 + 'defaults' => [
20 + 'guard' => 'web',
21 + 'passwords' => 'users',
22 + ],
19 23
20 /* 24 /*
21 |-------------------------------------------------------------------------- 25 |--------------------------------------------------------------------------
22 - | Authentication Model 26 + | Authentication Guards
23 |-------------------------------------------------------------------------- 27 |--------------------------------------------------------------------------
24 | 28 |
25 - | When using the "Eloquent" authentication driver, we need to know which 29 + | Next, you may define every authentication guard for your application.
26 - | Eloquent model should be used to retrieve your users. Of course, it 30 + | Of course, a great default configuration has been defined for you
27 - | is often just the "User" model but you may use whatever you like. 31 + | here which uses session storage and the Eloquent user provider.
32 + |
33 + | All authentication drivers have a user provider. This defines how the
34 + | users are actually retrieved out of your database or other storage
35 + | mechanisms used by this application to persist your user's data.
36 + |
37 + | Supported: "session", "token"
28 | 38 |
29 */ 39 */
30 40
31 - 'model' => BookStack\User::class, 41 + 'guards' => [
42 + 'web' => [
43 + 'driver' => 'session',
44 + 'provider' => 'users',
45 + ],
46 +
47 + 'api' => [
48 + 'driver' => 'token',
49 + 'provider' => 'users',
50 + ],
51 + ],
32 52
33 /* 53 /*
34 |-------------------------------------------------------------------------- 54 |--------------------------------------------------------------------------
35 - | Authentication Table 55 + | User Providers
36 |-------------------------------------------------------------------------- 56 |--------------------------------------------------------------------------
37 | 57 |
38 - | When using the "Database" authentication driver, we need to know which 58 + | All authentication drivers have a user provider. This defines how the
39 - | table should be used to retrieve your users. We have chosen a basic 59 + | users are actually retrieved out of your database or other storage
40 - | default value but you may easily change it to any table you like. 60 + | mechanisms used by this application to persist your user's data.
61 + |
62 + | If you have multiple user tables or models you may configure multiple
63 + | sources which represent each model / table. These sources may then
64 + | be assigned to any extra authentication guards you have defined.
65 + |
66 + | Supported: "database", "eloquent"
41 | 67 |
42 */ 68 */
43 69
44 - 'table' => 'users', 70 + 'providers' => [
71 + 'users' => [
72 + 'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
73 + 'model' => BookStack\User::class,
74 + ],
75 +
76 + // 'users' => [
77 + // 'driver' => 'database',
78 + // 'table' => 'users',
79 + // ],
80 + ],
45 81
46 /* 82 /*
47 |-------------------------------------------------------------------------- 83 |--------------------------------------------------------------------------
48 - | Password Reset Settings 84 + | Resetting Passwords
49 |-------------------------------------------------------------------------- 85 |--------------------------------------------------------------------------
50 | 86 |
51 | Here you may set the options for resetting passwords including the view 87 | Here you may set the options for resetting passwords including the view
52 - | that is your password reset e-mail. You can also set the name of the 88 + | that is your password reset e-mail. You may also set the name of the
53 | table that maintains all of the reset tokens for your application. 89 | table that maintains all of the reset tokens for your application.
54 | 90 |
91 + | You may specify multiple password reset configurations if you have more
92 + | than one user table or model in the application and you want to have
93 + | separate password reset settings based on the specific user types.
94 + |
55 | The expire time is the number of minutes that the reset token should be 95 | The expire time is the number of minutes that the reset token should be
56 | considered valid. This security feature keeps tokens short-lived so 96 | considered valid. This security feature keeps tokens short-lived so
57 | they have less time to be guessed. You may change this as needed. 97 | they have less time to be guessed. You may change this as needed.
58 | 98 |
59 */ 99 */
60 100
61 - 'password' => [ 101 + 'passwords' => [
102 + 'users' => [
103 + 'provider' => 'users',
62 'email' => 'emails.password', 104 'email' => 'emails.password',
63 'table' => 'password_resets', 105 'table' => 'password_resets',
64 'expire' => 60, 106 'expire' => 60,
65 ], 107 ],
108 + ],
66 109
67 ]; 110 ];
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -15,7 +15,18 @@ return [ ...@@ -15,7 +15,18 @@ return [
15 | 15 |
16 */ 16 */
17 17
18 - 'default' => 'local', 18 + 'default' => env('STORAGE_TYPE', 'local'),
19 +
20 + /*
21 + |--------------------------------------------------------------------------
22 + | Storage URL
23 + |--------------------------------------------------------------------------
24 + |
25 + | This is the url to where the storage is located for when using an external
26 + | file storage service, such as s3, to store publicly accessible assets.
27 + |
28 + */
29 + 'url' => env('STORAGE_URL', false),
19 30
20 /* 31 /*
21 |-------------------------------------------------------------------------- 32 |--------------------------------------------------------------------------
......
...@@ -13,6 +13,8 @@ return [ ...@@ -13,6 +13,8 @@ return [
13 | to have a conventional place to find your various credentials. 13 | to have a conventional place to find your various credentials.
14 | 14 |
15 */ 15 */
16 + 'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
17 + 'callback_url' => env('APP_URL', false),
16 18
17 'mailgun' => [ 19 'mailgun' => [
18 'domain' => '', 20 'domain' => '',
...@@ -47,4 +49,13 @@ return [ ...@@ -47,4 +49,13 @@ return [
47 'redirect' => env('APP_URL') . '/login/service/google/callback', 49 'redirect' => env('APP_URL') . '/login/service/google/callback',
48 ], 50 ],
49 51
52 + 'ldap' => [
53 + 'server' => env('LDAP_SERVER', false),
54 + 'dn' => env('LDAP_DN', false),
55 + 'pass' => env('LDAP_PASS', false),
56 + 'base_dn' => env('LDAP_BASE_DN', false),
57 + 'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
58 + 'version' => env('LDAP_VERSION', false)
59 + ]
60 +
50 ]; 61 ];
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddExternalAuthToUsers extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('users', function (Blueprint $table) {
16 + $table->string('external_auth_id')->index();
17 + });
18 + }
19 +
20 + /**
21 + * Reverse the migrations.
22 + *
23 + * @return void
24 + */
25 + public function down()
26 + {
27 + Schema::table('users', function (Blueprint $table) {
28 + $table->dropColumn('external_auth_id');
29 + });
30 + }
31 +}
...@@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) { ...@@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) {
21 elixir(function(mix) { 21 elixir(function(mix) {
22 mix.sass('styles.scss') 22 mix.sass('styles.scss')
23 .sass('print-styles.scss') 23 .sass('print-styles.scss')
24 + .sass('export-styles.scss')
24 .browserify('global.js', 'public/js/common.js') 25 .browserify('global.js', 'public/js/common.js')
25 .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']); 26 .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
26 }); 27 });
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
26 <env name="QUEUE_DRIVER" value="sync"/> 26 <env name="QUEUE_DRIVER" value="sync"/>
27 <env name="DB_CONNECTION" value="mysql_testing"/> 27 <env name="DB_CONNECTION" value="mysql_testing"/>
28 <env name="MAIL_PRETEND" value="true"/> 28 <env name="MAIL_PRETEND" value="true"/>
29 + <env name="AUTH_METHOD" value="standard"/>
29 <env name="DISABLE_EXTERNAL_SERVICES" value="false"/> 30 <env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
30 </php> 31 </php>
31 </phpunit> 32 </phpunit>
......
...@@ -2,12 +2,24 @@ ...@@ -2,12 +2,24 @@
2 2
3 A platform to create documentation/wiki content. General information about BookStack can be found at https://www.bookstackapp.com/ 3 A platform to create documentation/wiki content. General information about BookStack can be found at https://www.bookstackapp.com/
4 4
5 +1. [Requirements](#requirements)
6 +2. [Installation](#installation)
7 + - [Server Rewrite Rules](#url-rewrite-rules)
8 +3. [Updating](#updating-bookstack)
9 +4. [Social Authentication](#social-authentication)
10 + - [Google](#google)
11 + - [GitHub](#github)
12 +5. [LDAP Authentication](#ldap-authentication)
13 +6. [Testing](#testing)
14 +7. [License](#license)
15 +8. [Attribution](#attribution)
16 +
5 17
6 ## Requirements 18 ## Requirements
7 19
8 BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing. 20 BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing.
9 21
10 -* PHP >= 5.5.9 22 +* PHP >= 5.5.9, Will need to be usable from the command line.
11 * OpenSSL PHP Extension 23 * OpenSSL PHP Extension
12 * PDO PHP Extension 24 * PDO PHP Extension
13 * MBstring PHP Extension 25 * MBstring PHP Extension
...@@ -21,7 +33,7 @@ BookStack has similar requirements to Laravel. On top of those are some front-en ...@@ -21,7 +33,7 @@ BookStack has similar requirements to Laravel. On top of those are some front-en
21 33
22 ## Installation 34 ## Installation
23 35
24 -Ensure the requirements are met before installing. 36 +Ensure the above requirements are met before installing. Currently BookStack requires its own domain/subdomain and will not work in a site subdirectory.
25 37
26 This project currently uses the `release` branch of this repository as a stable channel for providing updates. 38 This project currently uses the `release` branch of this repository as a stable channel for providing updates.
27 39
...@@ -38,7 +50,7 @@ git clone https://github.com/ssddanbrown/BookStack.git --branch release --single ...@@ -38,7 +50,7 @@ git clone https://github.com/ssddanbrown/BookStack.git --branch release --single
38 4. Ensure the `storage` & `bootstrap/cache` folders are writable by the web server. 50 4. Ensure the `storage` & `bootstrap/cache` folders are writable by the web server.
39 5. In the application root, Run `php artisan key:generate` to generate a unique application key. 51 5. In the application root, Run `php artisan key:generate` to generate a unique application key.
40 6. If not using apache or if `.htaccess` files are disabled you will have to create some URL rewrite rules as shown below. 52 6. If not using apache or if `.htaccess` files are disabled you will have to create some URL rewrite rules as shown below.
41 -7. Run `php migrate` to update the database. 53 +7. Run `php artisan migrate` to update the database.
42 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. 54 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.
43 55
44 #### URL Rewrite rules 56 #### URL Rewrite rules
...@@ -59,6 +71,78 @@ location / { ...@@ -59,6 +71,78 @@ location / {
59 try_files $uri $uri/ /index.php?$query_string; 71 try_files $uri $uri/ /index.php?$query_string;
60 } 72 }
61 ``` 73 ```
74 +## Updating BookStack
75 +
76 +To update BookStack you can run the following command in the root directory of the application:
77 +```
78 +git pull origin release && composer install && php artisan migrate
79 +```
80 +This command will update the repository that was created in the installation, install the PHP dependencies using `composer` then run the database migrations.
81 +
82 +## Social Authentication
83 +
84 +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:
85 +
86 +### Google
87 +
88 +1. Open the [Google Developers Console](https://console.developers.google.com/).
89 +2. Create a new project (May have to wait a short while for it to be created).
90 +3. Select 'Enable and manage APIs'.
91 +4. Enable the 'Google+ API'.
92 +5. In 'Credentials' choose the 'OAuth consent screen' tab and enter a product name ('BookStack' or your custom set name).
93 +6. Back in the 'Credentials' tab click 'New credentials' > 'OAuth client ID'.
94 +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:
95 + - `https://example.com/login/service/google/callback`
96 + - `https://example.com/register/service/google/callback`
97 +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.
98 +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`.
99 +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.
100 +
101 +### Github
102 +
103 +1. While logged in, open up your [GitHub developer applications](https://github.com/settings/developers).
104 +2. Click 'Register new application'.
105 +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'.
106 +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.
107 +5. Set the 'APP_URL' environment variable to be the same domain as you entered in step 3.
108 +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.
109 +
110 +## LDAP Authentication
111 +
112 +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.
113 +
114 +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.
115 +
116 +To set up LDAP-based authentication add or modify the following variables in your `.env` file:
117 +
118 +```
119 +# General auth
120 +AUTH_METHOD=ldap
121 +
122 +# The LDAP host, Adding a port is optional
123 +LDAP_SERVER=ldap://example.com:389
124 +
125 +# The base DN from where users will be searched within.
126 +LDAP_BASE_DN=ou=People,dc=example,dc=com
127 +
128 +# The full DN and password of the user used to search the server
129 +# Can both be left as false to bind anonymously
130 +LDAP_DN=false
131 +LDAP_PASS=false
132 +
133 +# A filter to use when searching for users
134 +# The user-provided user-name used to replace any occurrences of '${user}'
135 +LDAP_USER_FILTER=(&(uid=${user}))
136 +
137 +# Set the LDAP version to use when connecting to the server.
138 +LDAP_VERSION=false
139 +```
140 +
141 +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.
142 +
143 +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.
144 +
145 +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.
62 146
63 ## Testing 147 ## Testing
64 148
...@@ -71,7 +155,7 @@ php artisan migrate --database=mysql_testing ...@@ -71,7 +155,7 @@ php artisan migrate --database=mysql_testing
71 php artisan db:seed --class=DummyContentSeeder --database=mysql_testing 155 php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
72 ``` 156 ```
73 157
74 -Once done you can run `phpunit` in the application root directory to run all tests. 158 +Once done you can run `phpunit` (or `./vendor/bin/phpunit` if `phpunit` is not found) in the application root directory to run all tests.
75 159
76 ## License 160 ## License
77 161
......
...@@ -56,6 +56,7 @@ module.exports = function (ngApp) { ...@@ -56,6 +56,7 @@ module.exports = function (ngApp) {
56 var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false'; 56 var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
57 scope.image = scope.currentImage; 57 scope.image = scope.currentImage;
58 scope.value = scope.currentImage || ''; 58 scope.value = scope.currentImage || '';
59 + if (usingIds) scope.value = scope.currentId;
59 60
60 function setImage(imageModel, imageUrl) { 61 function setImage(imageModel, imageUrl) {
61 scope.image = imageUrl; 62 scope.image = imageUrl;
......
1 +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
2 +@font-face {
3 + font-family: 'Roboto';
4 + src: url('/fonts/roboto-bold-webfont.eot');
5 + src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
6 + url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
7 + url('/fonts/roboto-bold-webfont.woff') format('woff'),
8 + url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
9 + url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
10 + font-weight: bold;
11 + font-style: normal;
12 +}
13 +
14 +@font-face {
15 + font-family: 'Roboto';
16 + src: url('/fonts/roboto-bolditalic-webfont.eot');
17 + src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
18 + url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
19 + url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
20 + url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
21 + url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
22 + font-weight: bold;
23 + font-style: italic;
24 +}
25 +
26 +@font-face {
27 + font-family: 'Roboto';
28 + src: url('/fonts/roboto-italic-webfont.eot');
29 + src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
30 + url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
31 + url('/fonts/roboto-italic-webfont.woff') format('woff'),
32 + url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
33 + url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
34 + font-weight: normal;
35 + font-style: italic;
36 +}
37 +
38 +@font-face {
39 + font-family: 'Roboto';
40 + src: url('/fonts/roboto-light-webfont.eot');
41 + src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
42 + url('/fonts/roboto-light-webfont.woff2') format('woff2'),
43 + url('/fonts/roboto-light-webfont.woff') format('woff'),
44 + url('/fonts/roboto-light-webfont.ttf') format('truetype'),
45 + url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
46 + font-weight: 300;
47 + font-style: normal;
48 +}
49 +
50 +@font-face {
51 + font-family: 'Roboto';
52 + src: url('/fonts/roboto-lightitalic-webfont.eot');
53 + src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
54 + url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
55 + url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
56 + url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
57 + url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
58 + font-weight: 300;
59 + font-style: italic;
60 +}
61 +
62 +@font-face {
63 + font-family: 'Roboto';
64 + src: url('/fonts/roboto-medium-webfont.eot');
65 + src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
66 + url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
67 + url('/fonts/roboto-medium-webfont.woff') format('woff'),
68 + url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
69 + url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
70 + font-weight: 500;
71 + font-style: normal;
72 +}
73 +
74 +@font-face {
75 + font-family: 'Roboto';
76 + src: url('/fonts/roboto-mediumitalic-webfont.eot');
77 + src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
78 + url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
79 + url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
80 + url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
81 + url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
82 + font-weight: 500;
83 + font-style: italic;
84 +}
85 +
86 +@font-face {
87 + font-family: 'Roboto';
88 + src: url('/fonts/roboto-regular-webfont.eot');
89 + src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
90 + url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
91 + url('/fonts/roboto-regular-webfont.woff') format('woff'),
92 + url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
93 + url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
94 + font-weight: normal;
95 + font-style: normal;
96 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -161,6 +161,12 @@ form.search-box { ...@@ -161,6 +161,12 @@ form.search-box {
161 padding: $-xs 0; 161 padding: $-xs 0;
162 color: #555; 162 color: #555;
163 text-align: left !important; 163 text-align: left !important;
164 + &.wide {
165 + min-width: 220px;
166 + }
167 + .text-muted {
168 + color: #999;
169 + }
164 a { 170 a {
165 display: block; 171 display: block;
166 padding: $-xs $-m; 172 padding: $-xs $-m;
...@@ -187,7 +193,7 @@ form.search-box { ...@@ -187,7 +193,7 @@ form.search-box {
187 } 193 }
188 194
189 .faded { 195 .faded {
190 - a, button, span { 196 + a, button, span, span > div {
191 color: #666; 197 color: #666;
192 } 198 }
193 .text-button { 199 .text-button {
......
...@@ -27,8 +27,8 @@ $-xs: 6px; ...@@ -27,8 +27,8 @@ $-xs: 6px;
27 $-xxs: 3px; 27 $-xxs: 3px;
28 28
29 // Fonts 29 // Fonts
30 -$heading: 'Roboto', Helvetica, Arial, sans-serif; 30 +$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
31 -$text: 'Roboto', Helvetica, Arial, sans-serif; 31 +$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
32 $fs-m: 15px; 32 $fs-m: 15px;
33 $fs-s: 14px; 33 $fs-s: 14px;
34 34
...@@ -52,101 +52,3 @@ $text-light: #EEE; ...@@ -52,101 +52,3 @@ $text-light: #EEE;
52 $bs-light: 0 0 4px 1px #CCC; 52 $bs-light: 0 0 4px 1px #CCC;
53 $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26); 53 $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
54 $bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); 54 $bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
55 -
56 -
57 -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
58 -@font-face {
59 - font-family: 'Roboto';
60 - src: url('/fonts/roboto-bold-webfont.eot');
61 - src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
62 - url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
63 - url('/fonts/roboto-bold-webfont.woff') format('woff'),
64 - url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
65 - url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
66 - font-weight: bold;
67 - font-style: normal;
68 -}
69 -
70 -@font-face {
71 - font-family: 'Roboto';
72 - src: url('/fonts/roboto-bolditalic-webfont.eot');
73 - src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
74 - url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
75 - url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
76 - url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
77 - url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
78 - font-weight: bold;
79 - font-style: italic;
80 -}
81 -
82 -@font-face {
83 - font-family: 'Roboto';
84 - src: url('/fonts/roboto-italic-webfont.eot');
85 - src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
86 - url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
87 - url('/fonts/roboto-italic-webfont.woff') format('woff'),
88 - url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
89 - url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
90 - font-weight: normal;
91 - font-style: italic;
92 -}
93 -
94 -@font-face {
95 - font-family: 'Roboto';
96 - src: url('/fonts/roboto-light-webfont.eot');
97 - src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
98 - url('/fonts/roboto-light-webfont.woff2') format('woff2'),
99 - url('/fonts/roboto-light-webfont.woff') format('woff'),
100 - url('/fonts/roboto-light-webfont.ttf') format('truetype'),
101 - url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
102 - font-weight: 300;
103 - font-style: normal;
104 -}
105 -
106 -@font-face {
107 - font-family: 'Roboto';
108 - src: url('/fonts/roboto-lightitalic-webfont.eot');
109 - src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
110 - url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
111 - url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
112 - url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
113 - url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
114 - font-weight: 300;
115 - font-style: italic;
116 -}
117 -
118 -@font-face {
119 - font-family: 'Roboto';
120 - src: url('/fonts/roboto-medium-webfont.eot');
121 - src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
122 - url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
123 - url('/fonts/roboto-medium-webfont.woff') format('woff'),
124 - url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
125 - url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
126 - font-weight: 500;
127 - font-style: normal;
128 -}
129 -
130 -@font-face {
131 - font-family: 'Roboto';
132 - src: url('/fonts/roboto-mediumitalic-webfont.eot');
133 - src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
134 - url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
135 - url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
136 - url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
137 - url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
138 - font-weight: 500;
139 - font-style: italic;
140 -}
141 -
142 -@font-face {
143 - font-family: 'Roboto';
144 - src: url('/fonts/roboto-regular-webfont.eot');
145 - src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
146 - url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
147 - url('/fonts/roboto-regular-webfont.woff') format('woff'),
148 - url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
149 - url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
150 - font-weight: normal;
151 - font-style: normal;
152 -}
......
1 +//@import "reset";
2 +@import "variables";
3 +@import "mixins";
4 +@import "html";
5 +@import "text";
6 +@import "grid";
7 +@import "blocks";
8 +@import "forms";
9 +@import "tables";
10 +@import "header";
11 +@import "lists";
12 +@import "pages";
...\ No newline at end of file ...\ No newline at end of file
1 @import "reset"; 1 @import "reset";
2 @import "variables"; 2 @import "variables";
3 +@import "fonts";
3 @import "mixins"; 4 @import "mixins";
4 @import "html"; 5 @import "html";
5 @import "text"; 6 @import "text";
......
1 +<div class="form-group">
2 + <label for="username">Username</label>
3 + @include('form/text', ['name' => 'username', 'tabindex' => 1])
4 +</div>
5 +
6 +@if(session('request-email', false) === true)
7 + <div class="form-group">
8 + <label for="email">Email</label>
9 + @include('form/text', ['name' => 'email', 'tabindex' => 1])
10 + <span class="text-neg">
11 + Please enter an email to use for this account.
12 + </span>
13 + </div>
14 +@endif
15 +
16 +<div class="form-group">
17 + <label for="password">Password</label>
18 + @include('form/password', ['name' => 'password', 'tabindex' => 2])
19 +</div>
...\ No newline at end of file ...\ No newline at end of file
1 +<div class="form-group">
2 + <label for="email">Email</label>
3 + @include('form/text', ['name' => 'email', 'tabindex' => 1])
4 +</div>
5 +
6 +<div class="form-group">
7 + <label for="password">Password</label>
8 + @include('form/password', ['name' => 'password', 'tabindex' => 2])
9 + <span class="block small"><a href="/password/email">Forgot Password?</a></span>
10 +</div>
...\ No newline at end of file ...\ No newline at end of file
...@@ -15,16 +15,8 @@ ...@@ -15,16 +15,8 @@
15 <form action="/login" method="POST" id="login-form"> 15 <form action="/login" method="POST" id="login-form">
16 {!! csrf_field() !!} 16 {!! csrf_field() !!}
17 17
18 - <div class="form-group">
19 - <label for="email">Email</label>
20 - @include('form/text', ['name' => 'email', 'tabindex' => 1])
21 - </div>
22 18
23 - <div class="form-group"> 19 + @include('auth/forms/login/' . $authMethod)
24 - <label for="password">Password</label>
25 - @include('form/password', ['name' => 'password', 'tabindex' => 2])
26 - <span class="block small"><a href="/password/email">Forgot Password?</a></span>
27 - </div>
28 20
29 <div class="form-group"> 21 <div class="form-group">
30 <label for="remember" class="inline">Remember Me</label> 22 <label for="remember" class="inline">Remember Me</label>
...@@ -34,7 +26,7 @@ ...@@ -34,7 +26,7 @@
34 26
35 27
36 <div class="from-group"> 28 <div class="from-group">
37 - <button class="button block pos" tabindex="3">Sign In</button> 29 + <button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> Sign In</button>
38 </div> 30 </div>
39 </form> 31 </form>
40 32
......
1 +<!doctype html>
2 +<html lang="en">
3 +<head>
4 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5 + <title>{{ $page->name }}</title>
6 +
7 + <style>
8 + {!! $css !!}
9 + </style>
10 + @yield('head')
11 +</head>
12 +<body>
13 +<div class="container" id="page-show">
14 + <div class="row">
15 + <div class="col-md-8 col-md-offset-2">
16 + <div class="page-content">
17 +
18 + @include('pages/page-display')
19 +
20 + <hr>
21 +
22 + <p class="text-muted small">
23 + Created {{$page->created_at->toDayDateTimeString()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
24 + <br>
25 + Last Updated {{$page->updated_at->toDayDateTimeString()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif
26 + </p>
27 +
28 + </div>
29 + </div>
30 + </div>
31 +</div>
32 +</body>
33 +</html>
1 +@extends('pages/export')
2 +
3 +@section('head')
4 + <style>
5 + body {
6 + font-size: 15px;
7 + line-height: 1;
8 + }
9 +
10 + h1, h2, h3, h4, h5, h6 {
11 + line-height: 1;
12 + }
13 +
14 + table {
15 + max-width: 800px !important;
16 + font-size: 0.8em;
17 + width: auto !important;
18 + }
19 +
20 + table td {
21 + width: auto !important;
22 + }
23 + </style>
24 +@stop
...\ No newline at end of file ...\ No newline at end of file
...@@ -19,6 +19,14 @@ ...@@ -19,6 +19,14 @@
19 </div> 19 </div>
20 <div class="col-sm-6 faded"> 20 <div class="col-sm-6 faded">
21 <div class="action-buttons"> 21 <div class="action-buttons">
22 + <span dropdown class="dropdown-container">
23 + <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
24 + <ul class="wide">
25 + <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li>
26 + <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li>
27 + <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li>
28 + </ul>
29 + </span>
22 @if($currentUser->can('page-update')) 30 @if($currentUser->can('page-update'))
23 <a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a> 31 <a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
24 <a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a> 32 <a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
......
...@@ -5,19 +5,19 @@ ...@@ -5,19 +5,19 @@
5 5
6 <!-- Meta --> 6 <!-- Meta -->
7 <meta name="viewport" content="width=device-width"> 7 <meta name="viewport" content="width=device-width">
8 + <meta name="token" content="{{ csrf_token() }}">
8 <meta charset="utf-8"> 9 <meta charset="utf-8">
9 10
10 <!-- Styles and Fonts --> 11 <!-- Styles and Fonts -->
11 <link rel="stylesheet" href="{{ versioned_asset('css/styles.css') }}"> 12 <link rel="stylesheet" href="{{ versioned_asset('css/styles.css') }}">
12 <link rel="stylesheet" media="print" href="{{ versioned_asset('css/print-styles.css') }}"> 13 <link rel="stylesheet" media="print" href="{{ versioned_asset('css/print-styles.css') }}">
13 - <link href='//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300' rel='stylesheet' type='text/css'>
14 <link rel="stylesheet" href="/libs/material-design-iconic-font/css/material-design-iconic-font.min.css"> 14 <link rel="stylesheet" href="/libs/material-design-iconic-font/css/material-design-iconic-font.min.css">
15 15
16 <!-- Scripts --> 16 <!-- Scripts -->
17 - <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> 17 + <script src="/libs/jquery/jquery.min.js?version=2.1.4"></script>
18 18
19 </head> 19 </head>
20 -<body class="@yield('body-class')" id="app"> 20 +<body class="@yield('body-class')" ng-app="bookStack">
21 21
22 @include('partials/notifications') 22 @include('partials/notifications')
23 23
...@@ -37,13 +37,16 @@ ...@@ -37,13 +37,16 @@
37 @yield('header-buttons') 37 @yield('header-buttons')
38 </div> 38 </div>
39 @if(isset($signedIn) && $signedIn) 39 @if(isset($signedIn) && $signedIn)
40 + <div class="dropdown-container" dropdown>
41 + <span class="user-name" dropdown-toggle>
40 <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}"> 42 <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
41 - <div class="dropdown-container" data-dropdown> 43 + <span class="name" ng-non-bindable>{{ $currentUser->name }}</span> <i class="zmdi zmdi-caret-down"></i>
42 - <span class="user-name" data-dropdown-toggle>
43 - {{ $currentUser->name }} <i class="zmdi zmdi-caret-down"></i>
44 </span> 44 </span>
45 <ul> 45 <ul>
46 <li> 46 <li>
47 + <a href="/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-lg"></i>Edit Profile</a>
48 + </li>
49 + <li>
47 <a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-lg"></i>Logout</a> 50 <a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-lg"></i>Logout</a>
48 </li> 51 </li>
49 </ul> 52 </ul>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 8
9 <form action="/users/create" method="post"> 9 <form action="/users/create" method="post">
10 {!! csrf_field() !!} 10 {!! csrf_field() !!}
11 - @include('users/form') 11 + @include('users.forms.' . $authMethod)
12 </form> 12 </form>
13 </div> 13 </div>
14 14
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
25 <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1> 25 <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
26 {!! csrf_field() !!} 26 {!! csrf_field() !!}
27 <input type="hidden" name="_method" value="put"> 27 <input type="hidden" name="_method" value="put">
28 - @include('users/form', ['model' => $user]) 28 + @include('users.forms.' . $authMethod, ['model' => $user])
29 29
30 </div> 30 </div>
31 <div class="col-md-6"> 31 <div class="col-md-6">
......
1 +<div class="form-group">
2 + <label for="name">Name</label>
3 + @include('form.text', ['name' => 'name'])
4 +</div>
5 +
6 +@if($currentUser->can('user-update'))
7 +<div class="form-group">
8 + <label for="email">Email</label>
9 + @include('form.text', ['name' => 'email'])
10 +</div>
11 +@endif
12 +
13 +@if($currentUser->can('user-update'))
14 + <div class="form-group">
15 + <label for="role">User Role</label>
16 + @include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name'])
17 + </div>
18 +@endif
19 +
20 +@if($currentUser->can('user-update'))
21 + <div class="form-group">
22 + <label for="external_auth_id">External Authentication ID</label>
23 + @include('form.text', ['name' => 'external_auth_id'])
24 + </div>
25 +@endif
26 +
27 +<div class="form-group">
28 + <a href="/users" class="button muted">Cancel</a>
29 + <button class="button pos" type="submit">Save</button>
30 +</div>
...\ No newline at end of file ...\ No newline at end of file
1 <div class="form-group"> 1 <div class="form-group">
2 <label for="name">Name</label> 2 <label for="name">Name</label>
3 - @include('form/text', ['name' => 'name']) 3 + @include('form.text', ['name' => 'name'])
4 </div> 4 </div>
5 5
6 <div class="form-group"> 6 <div class="form-group">
7 <label for="email">Email</label> 7 <label for="email">Email</label>
8 - @include('form/text', ['name' => 'email']) 8 + @include('form.text', ['name' => 'email'])
9 </div> 9 </div>
10 10
11 @if($currentUser->can('user-update')) 11 @if($currentUser->can('user-update'))
...@@ -25,12 +25,12 @@ ...@@ -25,12 +25,12 @@
25 25
26 <div class="form-group"> 26 <div class="form-group">
27 <label for="password">Password</label> 27 <label for="password">Password</label>
28 - @include('form/password', ['name' => 'password']) 28 + @include('form.password', ['name' => 'password'])
29 </div> 29 </div>
30 30
31 <div class="form-group"> 31 <div class="form-group">
32 <label for="password-confirm">Confirm Password</label> 32 <label for="password-confirm">Confirm Password</label>
33 - @include('form/password', ['name' => 'password-confirm']) 33 + @include('form.password', ['name' => 'password-confirm'])
34 </div> 34 </div>
35 35
36 <div class="form-group"> 36 <div class="form-group">
......
1 +*
2 +!.gitignore
...\ No newline at end of file ...\ No newline at end of file
...@@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions; ...@@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
7 class ActivityTrackingTest extends TestCase 7 class ActivityTrackingTest extends TestCase
8 { 8 {
9 9
10 - public function testRecentlyViewedBooks() 10 + public function test_recently_viewed_books()
11 { 11 {
12 $books = \BookStack\Book::all()->take(10); 12 $books = \BookStack\Book::all()->take(10);
13 13
...@@ -21,7 +21,7 @@ class ActivityTrackingTest extends TestCase ...@@ -21,7 +21,7 @@ class ActivityTrackingTest extends TestCase
21 ->seeInElement('#recents', $books[1]->name); 21 ->seeInElement('#recents', $books[1]->name);
22 } 22 }
23 23
24 - public function testPopularBooks() 24 + public function test_popular_books()
25 { 25 {
26 $books = \BookStack\Book::all()->take(10); 26 $books = \BookStack\Book::all()->take(10);
27 27
......
...@@ -5,23 +5,19 @@ use BookStack\EmailConfirmation; ...@@ -5,23 +5,19 @@ use BookStack\EmailConfirmation;
5 class AuthTest extends TestCase 5 class AuthTest extends TestCase
6 { 6 {
7 7
8 - public function testAuthWorking() 8 + public function test_auth_working()
9 { 9 {
10 $this->visit('/') 10 $this->visit('/')
11 ->seePageIs('/login'); 11 ->seePageIs('/login');
12 } 12 }
13 13
14 - public function testLogin() 14 + public function test_login()
15 { 15 {
16 - $this->visit('/')
17 - ->seePageIs('/login');
18 -
19 $this->login('admin@admin.com', 'password') 16 $this->login('admin@admin.com', 'password')
20 - ->seePageIs('/') 17 + ->seePageIs('/');
21 - ->see('BookStack');
22 } 18 }
23 19
24 - public function testPublicViewing() 20 + public function test_public_viewing()
25 { 21 {
26 $settings = app('BookStack\Services\SettingService'); 22 $settings = app('BookStack\Services\SettingService');
27 $settings->put('app-public', 'true'); 23 $settings->put('app-public', 'true');
...@@ -30,7 +26,7 @@ class AuthTest extends TestCase ...@@ -30,7 +26,7 @@ class AuthTest extends TestCase
30 ->see('Sign In'); 26 ->see('Sign In');
31 } 27 }
32 28
33 - public function testRegistrationShowing() 29 + public function test_registration_showing()
34 { 30 {
35 // Ensure registration form is showing 31 // Ensure registration form is showing
36 $this->setSettings(['registration-enabled' => 'true']); 32 $this->setSettings(['registration-enabled' => 'true']);
...@@ -40,7 +36,7 @@ class AuthTest extends TestCase ...@@ -40,7 +36,7 @@ class AuthTest extends TestCase
40 ->seePageIs('/register'); 36 ->seePageIs('/register');
41 } 37 }
42 38
43 - public function testNormalRegistration() 39 + public function test_normal_registration()
44 { 40 {
45 // Set settings and get user instance 41 // Set settings and get user instance
46 $this->setSettings(['registration-enabled' => 'true']); 42 $this->setSettings(['registration-enabled' => 'true']);
...@@ -58,7 +54,8 @@ class AuthTest extends TestCase ...@@ -58,7 +54,8 @@ class AuthTest extends TestCase
58 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]); 54 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
59 } 55 }
60 56
61 - public function testConfirmedRegistration() 57 +
58 + public function test_confirmed_registration()
62 { 59 {
63 // Set settings and get user instance 60 // Set settings and get user instance
64 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); 61 $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
...@@ -102,7 +99,32 @@ class AuthTest extends TestCase ...@@ -102,7 +99,32 @@ class AuthTest extends TestCase
102 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]); 99 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
103 } 100 }
104 101
105 - public function testUserCreation() 102 + public function test_restricted_registration()
103 + {
104 + $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
105 + $user = factory(\BookStack\User::class)->make();
106 + // Go through registration process
107 + $this->visit('/register')
108 + ->type($user->name, '#name')
109 + ->type($user->email, '#email')
110 + ->type($user->password, '#password')
111 + ->press('Create Account')
112 + ->seePageIs('/register')
113 + ->dontSeeInDatabase('users', ['email' => $user->email])
114 + ->see('That email domain does not have access to this application');
115 +
116 + $user->email = 'barry@example.com';
117 +
118 + $this->visit('/register')
119 + ->type($user->name, '#name')
120 + ->type($user->email, '#email')
121 + ->type($user->password, '#password')
122 + ->press('Create Account')
123 + ->seePageIs('/register/confirm')
124 + ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
125 + }
126 +
127 + public function test_user_creation()
106 { 128 {
107 $user = factory(\BookStack\User::class)->make(); 129 $user = factory(\BookStack\User::class)->make();
108 130
...@@ -120,7 +142,7 @@ class AuthTest extends TestCase ...@@ -120,7 +142,7 @@ class AuthTest extends TestCase
120 ->see($user->name); 142 ->see($user->name);
121 } 143 }
122 144
123 - public function testUserUpdating() 145 + public function test_user_updating()
124 { 146 {
125 $user = \BookStack\User::all()->last(); 147 $user = \BookStack\User::all()->last();
126 $password = $user->password; 148 $password = $user->password;
...@@ -136,7 +158,7 @@ class AuthTest extends TestCase ...@@ -136,7 +158,7 @@ class AuthTest extends TestCase
136 ->notSeeInDatabase('users', ['name' => $user->name]); 158 ->notSeeInDatabase('users', ['name' => $user->name]);
137 } 159 }
138 160
139 - public function testUserPasswordUpdate() 161 + public function test_user_password_update()
140 { 162 {
141 $user = \BookStack\User::all()->last(); 163 $user = \BookStack\User::all()->last();
142 $userProfilePage = '/users/' . $user->id; 164 $userProfilePage = '/users/' . $user->id;
...@@ -156,7 +178,7 @@ class AuthTest extends TestCase ...@@ -156,7 +178,7 @@ class AuthTest extends TestCase
156 $this->assertTrue(Hash::check('newpassword', $userPassword)); 178 $this->assertTrue(Hash::check('newpassword', $userPassword));
157 } 179 }
158 180
159 - public function testUserDeletion() 181 + public function test_user_deletion()
160 { 182 {
161 $userDetails = factory(\BookStack\User::class)->make(); 183 $userDetails = factory(\BookStack\User::class)->make();
162 $user = $this->getNewUser($userDetails->toArray()); 184 $user = $this->getNewUser($userDetails->toArray());
...@@ -170,7 +192,7 @@ class AuthTest extends TestCase ...@@ -170,7 +192,7 @@ class AuthTest extends TestCase
170 ->notSeeInDatabase('users', ['name' => $user->name]); 192 ->notSeeInDatabase('users', ['name' => $user->name]);
171 } 193 }
172 194
173 - public function testUserCannotBeDeletedIfLastAdmin() 195 + public function test_user_cannot_be_deleted_if_last_admin()
174 { 196 {
175 $adminRole = \BookStack\Role::getRole('admin'); 197 $adminRole = \BookStack\Role::getRole('admin');
176 // Ensure we currently only have 1 admin user 198 // Ensure we currently only have 1 admin user
...@@ -184,7 +206,7 @@ class AuthTest extends TestCase ...@@ -184,7 +206,7 @@ class AuthTest extends TestCase
184 ->see('You cannot delete the only admin'); 206 ->see('You cannot delete the only admin');
185 } 207 }
186 208
187 - public function testLogout() 209 + public function test_logout()
188 { 210 {
189 $this->asAdmin() 211 $this->asAdmin()
190 ->visit('/') 212 ->visit('/')
...@@ -200,7 +222,7 @@ class AuthTest extends TestCase ...@@ -200,7 +222,7 @@ class AuthTest extends TestCase
200 * @param string $password 222 * @param string $password
201 * @return $this 223 * @return $this
202 */ 224 */
203 - private function login($email, $password) 225 + protected function login($email, $password)
204 { 226 {
205 return $this->visit('/login') 227 return $this->visit('/login')
206 ->type($email, '#email') 228 ->type($email, '#email')
......
1 +<?php
2 +
3 +use BookStack\Services\LdapService;
4 +use BookStack\User;
5 +
6 +class LdapTest extends \TestCase
7 +{
8 +
9 + protected $mockLdap;
10 + protected $mockUser;
11 + protected $resourceId = 'resource-test';
12 +
13 + public function setUp()
14 + {
15 + parent::setUp();
16 + app('config')->set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']);
17 + $this->mockLdap = Mockery::mock(BookStack\Services\Ldap::class);
18 + $this->app['BookStack\Services\Ldap'] = $this->mockLdap;
19 + $this->mockUser = factory(User::class)->make();
20 + }
21 +
22 + public function test_login()
23 + {
24 + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
25 + $this->mockLdap->shouldReceive('setOption')->once();
26 + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
27 + ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
28 + ->andReturn(['count' => 1, 0 => [
29 + 'uid' => [$this->mockUser->name],
30 + 'cn' => [$this->mockUser->name],
31 + 'dn' => ['dc=test'.config('services.ldap.base_dn')]
32 + ]]);
33 + $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
34 +
35 + $this->visit('/login')
36 + ->see('Username')
37 + ->type($this->mockUser->name, '#username')
38 + ->type($this->mockUser->password, '#password')
39 + ->press('Sign In')
40 + ->seePageIs('/login')->see('Please enter an email to use for this account.');
41 +
42 + $this->type($this->mockUser->email, '#email')
43 + ->press('Sign In')
44 + ->seePageIs('/')
45 + ->see($this->mockUser->name)
46 + ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
47 + }
48 +
49 + public function test_initial_incorrect_details()
50 + {
51 + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
52 + $this->mockLdap->shouldReceive('setOption')->once();
53 + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
54 + ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
55 + ->andReturn(['count' => 1, 0 => [
56 + 'uid' => [$this->mockUser->name],
57 + 'cn' => [$this->mockUser->name],
58 + 'dn' => ['dc=test'.config('services.ldap.base_dn')]
59 + ]]);
60 + $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
61 +
62 + $this->visit('/login')
63 + ->see('Username')
64 + ->type($this->mockUser->name, '#username')
65 + ->type($this->mockUser->password, '#password')
66 + ->press('Sign In')
67 + ->seePageIs('/login')->see('These credentials do not match our records.')
68 + ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
69 + }
70 +
71 + public function test_create_user_form()
72 + {
73 + $this->asAdmin()->visit('/users/create')
74 + ->dontSee('Password')
75 + ->type($this->mockUser->name, '#name')
76 + ->type($this->mockUser->email, '#email')
77 + ->press('Save')
78 + ->see('The external auth id field is required.')
79 + ->type($this->mockUser->name, '#external_auth_id')
80 + ->press('Save')
81 + ->seePageIs('/users')
82 + ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
83 + }
84 +
85 + public function test_user_edit_form()
86 + {
87 + $editUser = User::all()->last();
88 + $this->asAdmin()->visit('/users/' . $editUser->id)
89 + ->see('Edit User')
90 + ->dontSee('Password')
91 + ->type('test_auth_id', '#external_auth_id')
92 + ->press('Save')
93 + ->seePageIs('/users')
94 + ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
95 + }
96 +
97 + public function test_registration_disabled()
98 + {
99 + $this->visit('/register')
100 + ->seePageIs('/login');
101 + }
102 +
103 + public function test_non_admins_cannot_change_auth_id()
104 + {
105 + $testUser = User::all()->last();
106 + $this->actingAs($testUser)->visit('/users/' . $testUser->id)
107 + ->dontSee('External Authentication');
108 + }
109 +
110 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
3 class SocialAuthTest extends TestCase 3 class SocialAuthTest extends TestCase
4 { 4 {
5 5
6 - public function testSocialRegistration() 6 + public function test_social_registration()
7 { 7 {
8 // http://docs.mockery.io/en/latest/reference/startup_methods.html 8 // http://docs.mockery.io/en/latest/reference/startup_methods.html
9 $user = factory(\BookStack\User::class)->make(); 9 $user = factory(\BookStack\User::class)->make();
10 10
11 $this->setSettings(['registration-enabled' => 'true']); 11 $this->setSettings(['registration-enabled' => 'true']);
12 - $this->setEnvironment(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']); 12 + config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
13 13
14 $mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory'); 14 $mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
15 $this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite; 15 $this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
...@@ -32,11 +32,4 @@ class SocialAuthTest extends TestCase ...@@ -32,11 +32,4 @@ class SocialAuthTest extends TestCase
32 $this->seeInDatabase('social_accounts', ['user_id' => $user->id]); 32 $this->seeInDatabase('social_accounts', ['user_id' => $user->id]);
33 } 33 }
34 34
35 - protected function setEnvironment($array)
36 - {
37 - foreach ($array as $key => $value) {
38 - putenv("$key=$value");
39 - }
40 - }
41 -
42 } 35 }
......
...@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\DB; ...@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\DB;
5 class EntityTest extends TestCase 5 class EntityTest extends TestCase
6 { 6 {
7 7
8 - public function testEntityCreation() 8 + public function test_entity_creation()
9 { 9 {
10 10
11 // Test Creation 11 // Test Creation
...@@ -51,7 +51,7 @@ class EntityTest extends TestCase ...@@ -51,7 +51,7 @@ class EntityTest extends TestCase
51 return \BookStack\Book::find($book->id); 51 return \BookStack\Book::find($book->id);
52 } 52 }
53 53
54 - public function testBookSortPageShows() 54 + public function test_book_sort_page_shows()
55 { 55 {
56 $books = \BookStack\Book::all(); 56 $books = \BookStack\Book::all();
57 $bookToSort = $books[0]; 57 $bookToSort = $books[0];
...@@ -65,7 +65,7 @@ class EntityTest extends TestCase ...@@ -65,7 +65,7 @@ class EntityTest extends TestCase
65 ->see($books[1]->name); 65 ->see($books[1]->name);
66 } 66 }
67 67
68 - public function testBookSortItemReturnsBookContent() 68 + public function test_book_sort_item_returns_book_content()
69 { 69 {
70 $books = \BookStack\Book::all(); 70 $books = \BookStack\Book::all();
71 $bookToSort = $books[0]; 71 $bookToSort = $books[0];
...@@ -155,7 +155,7 @@ class EntityTest extends TestCase ...@@ -155,7 +155,7 @@ class EntityTest extends TestCase
155 return $book; 155 return $book;
156 } 156 }
157 157
158 - public function testPageSearch() 158 + public function test_page_search()
159 { 159 {
160 $book = \BookStack\Book::all()->first(); 160 $book = \BookStack\Book::all()->first();
161 $page = $book->pages->first(); 161 $page = $book->pages->first();
...@@ -170,7 +170,7 @@ class EntityTest extends TestCase ...@@ -170,7 +170,7 @@ class EntityTest extends TestCase
170 ->seePageIs($page->getUrl()); 170 ->seePageIs($page->getUrl());
171 } 171 }
172 172
173 - public function testInvalidPageSearch() 173 + public function test_invalid_page_search()
174 { 174 {
175 $this->asAdmin() 175 $this->asAdmin()
176 ->visit('/') 176 ->visit('/')
...@@ -180,7 +180,7 @@ class EntityTest extends TestCase ...@@ -180,7 +180,7 @@ class EntityTest extends TestCase
180 ->seeStatusCode(200); 180 ->seeStatusCode(200);
181 } 181 }
182 182
183 - public function testEmptySearchRedirectsBack() 183 + public function test_empty_search_redirects_back()
184 { 184 {
185 $this->asAdmin() 185 $this->asAdmin()
186 ->visit('/') 186 ->visit('/')
...@@ -188,7 +188,7 @@ class EntityTest extends TestCase ...@@ -188,7 +188,7 @@ class EntityTest extends TestCase
188 ->seePageIs('/'); 188 ->seePageIs('/');
189 } 189 }
190 190
191 - public function testBookSearch() 191 + public function test_book_search()
192 { 192 {
193 $book = \BookStack\Book::all()->first(); 193 $book = \BookStack\Book::all()->first();
194 $page = $book->pages->last(); 194 $page = $book->pages->last();
...@@ -202,7 +202,7 @@ class EntityTest extends TestCase ...@@ -202,7 +202,7 @@ class EntityTest extends TestCase
202 ->see($chapter->name); 202 ->see($chapter->name);
203 } 203 }
204 204
205 - public function testEmptyBookSearchRedirectsBack() 205 + public function test_empty_book_search_redirects_back()
206 { 206 {
207 $book = \BookStack\Book::all()->first(); 207 $book = \BookStack\Book::all()->first();
208 $this->asAdmin() 208 $this->asAdmin()
...@@ -212,7 +212,7 @@ class EntityTest extends TestCase ...@@ -212,7 +212,7 @@ class EntityTest extends TestCase
212 } 212 }
213 213
214 214
215 - public function testEntitiesViewableAfterCreatorDeletion() 215 + public function test_entities_viewable_after_creator_deletion()
216 { 216 {
217 // Create required assets and revisions 217 // Create required assets and revisions
218 $creator = $this->getNewUser(); 218 $creator = $this->getNewUser();
...@@ -225,7 +225,7 @@ class EntityTest extends TestCase ...@@ -225,7 +225,7 @@ class EntityTest extends TestCase
225 $this->checkEntitiesViewable($entities); 225 $this->checkEntitiesViewable($entities);
226 } 226 }
227 227
228 - public function testEntitiesViewableAfterUpdaterDeletion() 228 + public function test_entities_viewable_after_updater_deletion()
229 { 229 {
230 // Create required assets and revisions 230 // Create required assets and revisions
231 $creator = $this->getNewUser(); 231 $creator = $this->getNewUser();
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 class PublicViewTest extends TestCase 3 class PublicViewTest extends TestCase
4 { 4 {
5 5
6 - public function testBooksViewable() 6 + public function test_books_viewable()
7 { 7 {
8 $this->setSettings(['app-public' => 'true']); 8 $this->setSettings(['app-public' => 'true']);
9 $books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get(); 9 $books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get();
...@@ -13,14 +13,14 @@ class PublicViewTest extends TestCase ...@@ -13,14 +13,14 @@ class PublicViewTest extends TestCase
13 $this->visit('/books') 13 $this->visit('/books')
14 ->seeStatusCode(200) 14 ->seeStatusCode(200)
15 ->see($books[0]->name) 15 ->see($books[0]->name)
16 - // Check indavidual book page is showing and it's child contents are visible. 16 + // Check individual book page is showing and it's child contents are visible.
17 ->click($bookToVisit->name) 17 ->click($bookToVisit->name)
18 ->seePageIs($bookToVisit->getUrl()) 18 ->seePageIs($bookToVisit->getUrl())
19 ->see($bookToVisit->name) 19 ->see($bookToVisit->name)
20 ->see($bookToVisit->chapters()->first()->name); 20 ->see($bookToVisit->chapters()->first()->name);
21 } 21 }
22 22
23 - public function testChaptersViewable() 23 + public function test_chapters_viewable()
24 { 24 {
25 $this->setSettings(['app-public' => 'true']); 25 $this->setSettings(['app-public' => 'true']);
26 $chapterToVisit = \BookStack\Chapter::first(); 26 $chapterToVisit = \BookStack\Chapter::first();
...@@ -30,7 +30,7 @@ class PublicViewTest extends TestCase ...@@ -30,7 +30,7 @@ class PublicViewTest extends TestCase
30 $this->visit($chapterToVisit->getUrl()) 30 $this->visit($chapterToVisit->getUrl())
31 ->seeStatusCode(200) 31 ->seeStatusCode(200)
32 ->see($chapterToVisit->name) 32 ->see($chapterToVisit->name)
33 - // Check indavidual chapter page is showing and it's child contents are visible. 33 + // Check individual chapter page is showing and it's child contents are visible.
34 ->see($pageToVisit->name) 34 ->see($pageToVisit->name)
35 ->click($pageToVisit->name) 35 ->click($pageToVisit->name)
36 ->see($chapterToVisit->book->name) 36 ->see($chapterToVisit->book->name)
......