1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-06-05 17:16:52 +03:00

Laravel 5.3 upgrade (#189)

* Started move to laravel 5.3

* Started updating login & registration flows for laravel 5.3 update

* Updated app emails to notification system

* Fixed registations bugs and removed email confirmation model

* Fixed large portion of laravel post-upgrade issues

* Fixed and tested LDAP process
This commit is contained in:
Dan Brown 2016-09-17 18:22:04 +01:00 committed by GitHub
parent 393f6047f2
commit 9dc9724e15
44 changed files with 1799 additions and 1110 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ Homestead.yaml
/storage/images /storage/images
_ide_helper.php _ide_helper.php
/storage/debugbar /storage/debugbar
.phpstorm.meta.php

View File

@ -1,16 +0,0 @@
<?php namespace BookStack;
class EmailConfirmation extends Model
{
protected $fillable = ['user_id', 'token'];
/**
* Get the user that this confirmation is attached to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace BookStack\Events;
abstract class Event
{
//
}

View File

@ -87,4 +87,20 @@ class Handler extends ExceptionHandler
} while ($e = $e->getPrevious()); } while ($e = $e->getPrevious());
return $message; return $message;
} }
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
} }

View File

@ -0,0 +1,33 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $socialAuthService;
protected $userRepo;
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
$this->socialAuthService = $socialAuthService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
parent::__construct();
}
public function username()
{
return config('auth.method') === 'standard' ? 'email' : 'username';
}
/**
* Overrides the action when a user is authenticated.
* If the user authenticated but does not exist in the user table we create them.
* @param Request $request
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
*/
protected function authenticated(Request $request, Authenticatable $user)
{
// Explicitly log them out for now if they do no exist.
if (!$user->exists) auth()->logout($user);
if (!$user->exists && $user->email === null && !$request->has('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
$user->email = $request->get('email');
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
}
/**
* Show the application login form.
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
}

View File

@ -1,62 +1,68 @@
<?php namespace BookStack\Http\Controllers\Auth; <?php
use BookStack\Exceptions\AuthException; namespace BookStack\Http\Controllers\Auth;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request; use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use BookStack\Services\EmailConfirmationService; use BookStack\Services\EmailConfirmationService;
use BookStack\Services\SocialAuthService; use BookStack\Services\SocialAuthService;
use BookStack\SocialAccount; use BookStack\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Validator; use Validator;
use BookStack\Http\Controllers\Controller; use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins; use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class AuthController extends Controller class RegisterController extends Controller
{ {
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Registration & Login Controller | Register Controller
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This controller handles the registration of new users, as well as the | This controller handles the registration of new users as well as their
| authentication of existing users. By default, this controller uses | validation and creation. By default this controller uses a trait to
| a simple trait to add these behaviors. Why don't you explore it? | provide this functionality without requiring any additional code.
| |
*/ */
use AuthenticatesAndRegistersUsers, ThrottlesLogins; use RegistersUsers;
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $username = 'email';
protected $socialAuthService; protected $socialAuthService;
protected $emailConfirmationService; protected $emailConfirmationService;
protected $userRepo; protected $userRepo;
/** /**
* Create a new authentication controller instance. * Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService * @param SocialAuthService $socialAuthService
* @param EmailConfirmationService $emailConfirmationService * @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo * @param UserRepo $userRepo
*/ */
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{ {
$this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]); $this->middleware('guest');
$this->socialAuthService = $socialAuthService; $this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService; $this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
$this->redirectTo = baseUrl('/');
$this->redirectPath = baseUrl('/'); $this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
$this->username = config('auth.method') === 'standard' ? 'email' : 'username'; $this->username = config('auth.method') === 'standard' ? 'email' : 'username';
parent::__construct(); parent::__construct();
} }
/** /**
* Get a validator for an incoming registration request. * Get a validator for an incoming registration request.
*
* @param array $data * @param array $data
* @return \Illuminate\Contracts\Validation\Validator * @return \Illuminate\Contracts\Validation\Validator
*/ */
@ -69,6 +75,10 @@ class AuthController extends Controller
]); ]);
} }
/**
* Check whether or not registrations are allowed in the app settings.
* @throws UserRegistrationException
*/
protected function checkRegistrationAllowed() protected function checkRegistrationAllowed()
{ {
if (!setting('registration-enabled')) { if (!setting('registration-enabled')) {
@ -78,7 +88,7 @@ class AuthController extends Controller
/** /**
* Show the application registration form. * Show the application registration form.
* @return \Illuminate\Http\Response * @return Response
*/ */
public function getRegister() public function getRegister()
{ {
@ -89,9 +99,10 @@ class AuthController extends Controller
/** /**
* Handle a registration request for the application. * Handle a registration request for the application.
* @param \Illuminate\Http\Request $request * @param Request|\Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return Response
* @throws UserRegistrationException * @throws UserRegistrationException
* @throws \Illuminate\Foundation\Validation\ValidationException
*/ */
public function postRegister(Request $request) public function postRegister(Request $request)
{ {
@ -108,66 +119,18 @@ class AuthController extends Controller
return $this->registerUser($userData); return $this->registerUser($userData);
} }
/** /**
* Overrides the action when a user is authenticated. * Create a new user instance after a valid registration.
* If the user authenticated but does not exist in the user table we create them. * @param array $data
* @param Request $request * @return User
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
*/ */
protected function authenticated(Request $request, Authenticatable $user) protected function create(array $data)
{ {
// Explicitly log them out for now if they do no exist. return User::create([
if (!$user->exists) auth()->logout($user); 'name' => $data['name'],
'email' => $data['email'],
if (!$user->exists && $user->email === null && !$request->has('email')) { 'password' => bcrypt($data['password']),
$request->flash(); ]);
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
$user->email = $request->get('email');
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function socialRegisterCallback($socialDriver)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
} }
/** /**
@ -176,7 +139,7 @@ class AuthController extends Controller
* @param bool|false|SocialAccount $socialAccount * @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException * @throws UserRegistrationException
* @throws \BookStack\Exceptions\ConfirmationEmailException * @throws ConfirmationEmailException
*/ */
protected function registerUser(array $userData, $socialAccount = false) protected function registerUser(array $userData, $socialAccount = false)
{ {
@ -213,18 +176,6 @@ class AuthController extends Controller
return view('auth/register-confirm'); return view('auth/register-confirm');
} }
/**
* View the confirmation email as a standard web page.
* @param $token
* @return \Illuminate\View\View
* @throws UserRegistrationException
*/
public function viewConfirmEmail($token)
{
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
return view('emails/email-confirmation', ['token' => $confirmation->token]);
}
/** /**
* Confirms an email via a token and logs the user into the system. * Confirms an email via a token and logs the user into the system.
* @param $token * @param $token
@ -237,7 +188,7 @@ class AuthController extends Controller
$user = $confirmation->user; $user = $confirmation->user;
$user->email_confirmed = true; $user->email_confirmed = true;
$user->save(); $user->save();
auth()->login($confirmation->user); auth()->login($user);
session()->flash('success', 'Your email has been confirmed!'); session()->flash('success', 'Your email has been confirmed!');
$this->emailConfirmationService->deleteConfirmationsByUser($user); $this->emailConfirmationService->deleteConfirmationsByUser($user);
return redirect($this->redirectPath); return redirect($this->redirectPath);
@ -269,28 +220,6 @@ class AuthController extends Controller
return redirect('/register/confirm'); return redirect('/register/confirm');
} }
/**
* Show the application login form.
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
/** /**
* Redirect to the social site for authentication intended to register. * Redirect to the social site for authentication intended to register.
* @param $socialDriver * @param $socialDriver
@ -334,4 +263,25 @@ class AuthController extends Controller
return $this->socialAuthService->detachSocialAccount($socialDriver); return $this->socialAuthService->detachSocialAccount($socialDriver);
} }
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function socialRegisterCallback($socialDriver)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
}
} }

View File

@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller; use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller class ResetPasswordController extends Controller
{ {
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -20,13 +20,14 @@ class PasswordController extends Controller
use ResetsPasswords; use ResetsPasswords;
protected $redirectTo = '/';
/** /**
* Create a new password controller instance. * Create a new controller instance.
*
* @return void
*/ */
public function __construct() public function __construct()
{ {
$this->middleware('guest'); $this->middleware('guest');
parent::__construct();
} }
} }

View File

@ -30,6 +30,8 @@ abstract class Controller extends BaseController
*/ */
public function __construct() public function __construct()
{ {
$this->middleware(function ($request, $next) {
// Get a user instance for the current user // Get a user instance for the current user
$user = auth()->user(); $user = auth()->user();
if (!$user) $user = User::getDefault(); if (!$user) $user = User::getDefault();
@ -41,6 +43,9 @@ abstract class Controller extends BaseController
// Share variables with controllers // Share variables with controllers
$this->currentUser = $user; $this->currentUser = $user;
$this->signedIn = auth()->check(); $this->signedIn = auth()->check();
return $next($request);
});
} }
/** /**

View File

@ -9,15 +9,32 @@ class Kernel extends HttpKernel
/** /**
* The application's global HTTP middleware stack. * The application's global HTTP middleware stack.
* *
* These middleware are run during every request to your application.
*
* @var array * @var array
*/ */
protected $middleware = [ protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class, \BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class, \BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
]; ];
/** /**
@ -26,6 +43,7 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'auth' => \BookStack\Http\Middleware\Authenticate::class, 'auth' => \BookStack\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,

View File

@ -33,7 +33,7 @@ class Authenticate
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) { if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
return redirect()->guest(baseUrl('/register/confirm/awaiting')); return redirect(baseUrl('/register/confirm/awaiting'));
} }
if ($this->auth->guest() && !setting('app-public')) { if ($this->auth->guest() && !setting('app-public')) {

View File

@ -34,7 +34,8 @@ class RedirectIfAuthenticated
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if ($this->auth->check()) { $requireConfirmation = setting('registration-confirmation');
if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
return redirect('/'); return redirect('/');
} }

View File

@ -1,21 +0,0 @@
<?php
namespace BookStack\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "queueOn" and "delay" queue helper methods.
|
*/
use Queueable;
}

View File

View File

@ -0,0 +1,48 @@
<?php
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification
{
public $token;
/**
* Create a new notification instance.
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject('Confirm your email on ' . session('app-name'))
->greeting('Thanks for joining ' . setting('app-name') . '!')
->line('Please confirm your email address by clicking the button below:')
->action('Confirm Email', baseUrl('/register/confirm/' . $this->token));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ResetPassword extends Notification
{
/**
* The password reset token.
*
* @var string
*/
public $token;
/**
* Create a notification instance.
*
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
return (new MailMessage)
->line('You are receiving this email because we received a password reset request for your account.')
->action('Reset Password', baseUrl('password/reset/' . $this->token))
->line('If you did not request a password reset, no further action is required.');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Broadcast::routes();
//
// /*
// * Authenticate the user's personal channel...
// */
// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
// return (int) $user->id === (int) $userId;
// });
}
}

View File

@ -21,13 +21,10 @@ class EventServiceProvider extends ServiceProvider
/** /**
* Register any other events for your application. * Register any other events for your application.
* *
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void * @return void
*/ */
public function boot(DispatcherContract $events) public function boot()
{ {
parent::boot($events); parent::boot();
//
} }
} }

View File

@ -1,11 +1,12 @@
<?php namespace BookStack\Providers; <?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
use Illuminate\Pagination\Paginator; use Illuminate\Pagination\Paginator;
class PaginationServiceProvider extends ServiceProvider class PaginationServiceProvider extends IlluminatePaginationServiceProvider
{ {
/** /**
* Register the service provider. * Register the service provider.
* *
@ -13,6 +14,10 @@ class PaginationServiceProvider extends ServiceProvider
*/ */
public function register() public function register()
{ {
Paginator::viewFactoryResolver(function () {
return $this->app['view'];
});
Paginator::currentPathResolver(function () { Paginator::currentPathResolver(function () {
return baseUrl($this->app['request']->path()); return baseUrl($this->app['request']->path());
}); });

View File

@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Routing\Router; use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Route;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
/** /**
* Define your route model bindings, pattern filters, etc. * Define your route model bindings, pattern filters, etc.
* *
* @param \Illuminate\Routing\Router $router
* @return void * @return void
*/ */
public function boot(Router $router) public function boot()
{ {
// parent::boot();
parent::boot($router);
} }
/** /**
* Define the routes for the application. * Define the routes for the application.
* *
* @param \Illuminate\Routing\Router $router
* @return void * @return void
*/ */
public function map(Router $router) public function map()
{ {
$router->group(['namespace' => $this->namespace], function ($router) { $this->mapWebRoutes();
require app_path('Http/routes.php'); // $this->mapApiRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::group([
'middleware' => 'api',
'namespace' => $this->namespace,
'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');
}); });
} }
} }

View File

@ -110,31 +110,6 @@ class PageRepo extends EntityRepo
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count(); return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
} }
/**
* Save a new page into the system.
* Input validation must be done beforehand.
* @param array $input
* @param Book $book
* @param int $chapterId
* @return Page
*/
public function saveNew(array $input, Book $book, $chapterId = null)
{
$page = $this->newFromInput($input);
$page->slug = $this->findSuitableSlug($page->name, $book->id);
if ($chapterId) $page->chapter_id = $chapterId;
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->created_by = auth()->user()->id;
$page->updated_by = auth()->user()->id;
$book->pages()->save($page);
return $page;
}
/** /**
* Publish a draft page to make it a normal page. * Publish a draft page to make it a normal page.
* Sets the slug and updates the content. * Sets the slug and updates the content.
@ -371,7 +346,7 @@ class PageRepo extends EntityRepo
*/ */
public function saveRevision(Page $page, $summary = null) public function saveRevision(Page $page, $summary = null)
{ {
$revision = $this->pageRevision->fill($page->toArray()); $revision = $this->pageRevision->newInstance($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = ''; if (setting('app-editor') !== 'markdown') $revision->markdown = '';
$revision->page_id = $page->id; $revision->page_id = $page->id;
$revision->slug = $page->slug; $revision->slug = $page->slug;
@ -381,11 +356,13 @@ class PageRepo extends EntityRepo
$revision->type = 'version'; $revision->type = 'version';
$revision->summary = $summary; $revision->summary = $summary;
$revision->save(); $revision->save();
// Clear old revisions // Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
$this->pageRevision->where('page_id', '=', $page->id) $this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
} }
return $revision; return $revision;
} }

View File

@ -1,30 +1,27 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Services;
use BookStack\Notifications\ConfirmEmail;
use BookStack\Repos\UserRepo;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use BookStack\EmailConfirmation;
use BookStack\Exceptions\ConfirmationEmailException; use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Setting;
use BookStack\User; use BookStack\User;
use Illuminate\Database\Connection as Database;
class EmailConfirmationService class EmailConfirmationService
{ {
protected $mailer; protected $db;
protected $emailConfirmation; protected $users;
/** /**
* EmailConfirmationService constructor. * EmailConfirmationService constructor.
* @param Mailer $mailer * @param Database $db
* @param EmailConfirmation $emailConfirmation * @param UserRepo $users
*/ */
public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation) public function __construct(Database $db, UserRepo $users)
{ {
$this->mailer = $mailer; $this->db = $db;
$this->emailConfirmation = $emailConfirmation; $this->users = $users;
} }
/** /**
@ -38,16 +35,28 @@ class EmailConfirmationService
if ($user->email_confirmed) { if ($user->email_confirmed) {
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login'); throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
} }
$this->deleteConfirmationsByUser($user); $this->deleteConfirmationsByUser($user);
$token = $this->createEmailConfirmation($user);
$user->notify(new ConfirmEmail($token));
}
/**
* Creates a new email confirmation in the database and returns the token.
* @param User $user
* @return string
*/
public function createEmailConfirmation(User $user)
{
$token = $this->getToken(); $token = $this->getToken();
$this->emailConfirmation->create([ $this->db->table('email_confirmations')->insert([
'user_id' => $user->id, 'user_id' => $user->id,
'token' => $token, 'token' => $token,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]); ]);
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) { return $token;
$appName = setting('app-name', 'BookStack');
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
});
} }
/** /**
@ -59,22 +68,24 @@ class EmailConfirmationService
*/ */
public function getEmailConfirmationFromToken($token) public function getEmailConfirmationFromToken($token)
{ {
$emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first(); $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
// If not found
// If not found show error
if ($emailConfirmation === null) { if ($emailConfirmation === null) {
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register'); throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
} }
// If more than a day old // If more than a day old
if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) { if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
$this->sendConfirmation($emailConfirmation->user); $user = $this->users->getById($emailConfirmation->user_id);
$this->sendConfirmation($user);
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm'); throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
} }
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
return $emailConfirmation; return $emailConfirmation;
} }
/** /**
* Delete all email confirmations that belong to a user. * Delete all email confirmations that belong to a user.
* @param User $user * @param User $user
@ -82,7 +93,7 @@ class EmailConfirmationService
*/ */
public function deleteConfirmationsByUser(User $user) public function deleteConfirmationsByUser(User $user)
{ {
return $this->emailConfirmation->where('user_id', '=', $user->id)->delete(); return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
} }
/** /**
@ -92,7 +103,7 @@ class EmailConfirmationService
protected function getToken() protected function getToken()
{ {
$token = str_random(24); $token = str_random(24);
while ($this->emailConfirmation->where('token', '=', $token)->exists()) { while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
$token = str_random(25); $token = str_random(25);
} }
return $token; return $token;

View File

@ -9,14 +9,15 @@ use BookStack\Page;
use BookStack\Role; use BookStack\Role;
use BookStack\User; use BookStack\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class PermissionService class PermissionService
{ {
protected $userRoles;
protected $isAdmin;
protected $currentAction; protected $currentAction;
protected $currentUser; protected $isAdminUser;
protected $userRoles = false;
protected $currentUserModel = false;
public $book; public $book;
public $chapter; public $chapter;
@ -37,12 +38,6 @@ class PermissionService
*/ */
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role) public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
{ {
$this->currentUser = auth()->user();
$userSet = $this->currentUser !== null;
$this->userRoles = false;
$this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
if (!$userSet) $this->currentUser = new User();
$this->jointPermission = $jointPermission; $this->jointPermission = $jointPermission;
$this->role = $role; $this->role = $role;
$this->book = $book; $this->book = $book;
@ -117,7 +112,7 @@ class PermissionService
} }
foreach ($this->currentUser->roles as $role) { foreach ($this->currentUser()->roles as $role) {
$roles[] = $role->id; $roles[] = $role->id;
} }
return $roles; return $roles;
@ -389,7 +384,11 @@ class PermissionService
*/ */
public function checkOwnableUserAccess(Ownable $ownable, $permission) public function checkOwnableUserAccess(Ownable $ownable, $permission)
{ {
if ($this->isAdmin) return true; if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission); $explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id); $baseQuery = $ownable->where('id', '=', $ownable->id);
@ -400,10 +399,10 @@ class PermissionService
// Handle non entity specific jointPermissions // Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) { if (in_array($explodedPermission[0], $nonJointPermissions)) {
$allPermission = $this->currentUser && $this->currentUser->can($permission . '-all'); $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
$ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own'); $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
$this->currentAction = 'view'; $this->currentAction = 'view';
$isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by; $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
return ($allPermission || ($isOwner && $ownPermission)); return ($allPermission || ($isOwner && $ownPermission));
} }
@ -413,7 +412,9 @@ class PermissionService
} }
return $this->entityRestrictionQuery($baseQuery)->count() > 0; $q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
$this->clean();
return $q;
} }
/** /**
@ -443,7 +444,7 @@ class PermissionService
*/ */
protected function entityRestrictionQuery($query) protected function entityRestrictionQuery($query)
{ {
return $query->where(function ($parentQuery) { $q = $query->where(function ($parentQuery) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) { $parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
$permissionQuery->whereIn('role_id', $this->getRoles()) $permissionQuery->whereIn('role_id', $this->getRoles())
->where('action', '=', $this->currentAction) ->where('action', '=', $this->currentAction)
@ -451,11 +452,13 @@ class PermissionService
$query->where('has_permission', '=', true) $query->where('has_permission', '=', true)
->orWhere(function ($query) { ->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id); ->where('created_by', '=', $this->currentUser()->id);
}); });
}); });
}); });
}); });
$this->clean();
return $q;
} }
/** /**
@ -469,9 +472,9 @@ class PermissionService
// Prevent drafts being visible to others. // Prevent drafts being visible to others.
$query = $query->where(function ($query) { $query = $query->where(function ($query) {
$query->where('draft', '=', false); $query->where('draft', '=', false);
if ($this->currentUser) { if ($this->currentUser()) {
$query->orWhere(function ($query) { $query->orWhere(function ($query) {
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id); $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
}); });
} }
}); });
@ -509,7 +512,10 @@ class PermissionService
*/ */
public function enforceEntityRestrictions($query, $action = 'view') public function enforceEntityRestrictions($query, $action = 'view')
{ {
if ($this->isAdmin) return $query; if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action; $this->currentAction = $action;
return $this->entityRestrictionQuery($query); return $this->entityRestrictionQuery($query);
} }
@ -524,11 +530,15 @@ class PermissionService
*/ */
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn) public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
{ {
if ($this->isAdmin) return $query; if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view'; $this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
return $query->where(function ($query) use ($tableDetails) { $q = $query->where(function ($query) use ($tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) { $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions') $permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
@ -538,12 +548,12 @@ class PermissionService
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id); ->where('created_by', '=', $this->currentUser()->id);
}); });
}); });
}); });
}); });
return $q;
} }
/** /**
@ -555,11 +565,15 @@ class PermissionService
*/ */
public function filterRelatedPages($query, $tableName, $entityIdColumn) public function filterRelatedPages($query, $tableName, $entityIdColumn)
{ {
if ($this->isAdmin) return $query; if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view'; $this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
return $query->where(function ($query) use ($tableDetails) { $q = $query->where(function ($query) use ($tableDetails) {
$query->where(function ($query) use (&$tableDetails) { $query->where(function ($query) use (&$tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) { $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions') $permissionQuery->select('id')->from('joint_permissions')
@ -570,12 +584,50 @@ class PermissionService
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id); ->where('created_by', '=', $this->currentUser()->id);
}); });
}); });
}); });
})->orWhere($tableDetails['entityIdColumn'], '=', 0); })->orWhere($tableDetails['entityIdColumn'], '=', 0);
}); });
$this->clean();
return $q;
}
/**
* Check if the current user is an admin.
* @return bool
*/
private function isAdmin()
{
if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false;
}
return $this->isAdminUser;
}
/**
* Get the current user
* @return User
*/
private function currentUser()
{
if ($this->currentUserModel === false) {
$this->currentUserModel = auth()->user() ? auth()->user() : new User();
}
return $this->currentUserModel;
}
/**
* Clean the cached user elements.
*/
private function clean()
{
$this->currentUserModel = false;
$this->userRoles = false;
$this->isAdminUser = null;
} }
} }

View File

@ -1,13 +1,15 @@
<?php namespace BookStack; <?php namespace BookStack;
use BookStack\Notifications\ResetPassword;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Notifications\Notifiable;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{ {
use Authenticatable, CanResetPassword; use Authenticatable, CanResetPassword, Notifiable;
/** /**
* The database table used by the model. * The database table used by the model.
@ -183,4 +185,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return ''; return '';
} }
/**
* Send the password reset notification.
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPassword($token));
}
} }

View File

@ -63,7 +63,7 @@ function userCan($permission, Ownable $ownable = null)
*/ */
function setting($key, $default = false) function setting($key, $default = false)
{ {
$settingService = app('BookStack\Services\SettingService'); $settingService = app(\BookStack\Services\SettingService::class);
return $settingService->get($key, $default); return $settingService->get($key, $default);
} }
@ -79,11 +79,17 @@ function baseUrl($path, $forceAppDomain = false)
if ($isFullUrl && !$forceAppDomain) return $path; if ($isFullUrl && !$forceAppDomain) return $path;
$path = trim($path, '/'); $path = trim($path, '/');
// Remove non-specified domain if forced and we have a domain
if ($isFullUrl && $forceAppDomain) { if ($isFullUrl && $forceAppDomain) {
$explodedPath = explode('/', $path); $explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3)); $path = implode('/', array_splice($explodedPath, 3));
} }
// Return normal url path if not specified in config
if (config('app.url') === '') {
return url($path);
}
return rtrim(config('app.url'), '/') . '/' . $path; return rtrim(config('app.url'), '/') . '/' . $path;
} }

View File

@ -5,23 +5,22 @@
"license": "MIT", "license": "MIT",
"type": "project", "type": "project",
"require": { "require": {
"php": ">=5.5.9", "php": ">=5.6.4",
"laravel/framework": "5.2.*", "laravel/framework": "^5.3.4",
"intervention/image": "^2.3", "intervention/image": "^2.3",
"laravel/socialite": "^2.0", "laravel/socialite": "^2.0",
"barryvdh/laravel-ide-helper": "^2.1", "barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.0", "barryvdh/laravel-debugbar": "^2.2.3",
"league/flysystem-aws-s3-v3": "^1.0", "league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "0.6.*", "barryvdh/laravel-dompdf": "^0.7",
"predis/predis": "^1.0" "predis/predis": "^1.1"
}, },
"require-dev": { "require-dev": {
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*", "mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0", "phpunit/phpunit": "~5.0",
"phpspec/phpspec": "~2.1", "symfony/css-selector": "3.1.*",
"symfony/dom-crawler": "~3.0", "symfony/dom-crawler": "3.1.*"
"symfony/css-selector": "~3.0"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [
@ -37,21 +36,19 @@
] ]
}, },
"scripts": { "scripts": {
"post-install-cmd": [
"php artisan clear-compiled",
"php artisan optimize"
],
"pre-update-cmd": [
"php artisan clear-compiled"
],
"post-update-cmd": [
"php artisan optimize"
],
"post-root-package-install": [ "post-root-package-install": [
"php -r \"copy('.env.example', '.env');\"" "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
], ],
"post-create-project-cmd": [ "post-create-project-cmd": [
"php artisan key:generate" "php artisan key:generate"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimize"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
] ]
}, },
"config": { "config": {

1457
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -138,6 +138,7 @@ return [
Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class, Illuminate\View\ViewServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class, Laravel\Socialite\SocialiteServiceProvider::class,
/** /**
@ -156,6 +157,7 @@ return [
BookStack\Providers\AuthServiceProvider::class, BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class, BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\BroadcastServiceProvider::class,
BookStack\Providers\EventServiceProvider::class, BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class, BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\CustomFacadeProvider::class, BookStack\Providers\CustomFacadeProvider::class,
@ -194,6 +196,7 @@ return [
'Lang' => Illuminate\Support\Facades\Lang::class, 'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class, 'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class, 'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class, 'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class, 'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class, 'Redirect' => Illuminate\Support\Facades\Redirect::class,

View File

@ -129,7 +129,7 @@ class AddRolesAndPermissions extends Migration
// Set all current users as admins // Set all current users as admins
// (At this point only the initially create user should be an admin) // (At this point only the initially create user should be an admin)
$users = DB::table('users')->get(); $users = DB::table('users')->get()->all();
foreach ($users as $user) { foreach ($users as $user) {
DB::table('role_user')->insert([ DB::table('role_user')->insert([
'role_id' => $adminId, 'role_id' => $adminId,

View File

@ -1,5 +0,0 @@
suites:
main:
namespace: BookStack
psr4_prefix: BookStack
src_path: app

View File

@ -0,0 +1,15 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

View File

@ -1,190 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
<meta name="viewport" content="width=device-width"
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
<title>Confirm Your Email At {{ setting('app-name')}}</title>
<style style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
* {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
font-size: 100%;
line-height: 1.6;
}
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
}
a {
color: #348eda;
}
.btn-primary {
text-decoration: none;
color: #FFF;
background-color: #348eda;
border: solid #348eda;
border-width: 10px 20px;
line-height: 2;
font-weight: bold;
margin-right: 10px;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 4px;
}
.btn-secondary {
text-decoration: none;
color: #FFF;
background-color: #aaa;
border: solid #aaa;
border-width: 10px 20px;
line-height: 2;
font-weight: bold;
margin-right: 10px;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 25px;
}
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.padding {
padding: 10px 0;
}
table.body-wrap {
width: 100%;
padding: 20px;
}
table.body-wrap .container {
border: 1px solid #f0f0f0;
}
h1,
h2,
h3 {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color: #444;
margin: 10px 0 10px;
line-height: 1.2;
font-weight: 200;
}
h1 {
font-size: 36px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
p,
ul,
ol {
margin-bottom: 10px;
font-weight: normal;
font-size: 14px;
color: #888888;
}
ul li,
ol li {
margin-left: 5px;
list-style-position: inside;
}
.container {
display: block !important;
max-width: 600px !important;
margin: 0 auto !important;
clear: both !important;
}
.body-wrap .container {
padding: 20px;
}
.content {
max-width: 600px;
margin: 0 auto;
display: block;
}
.content table {
width: 100%;
}
</style>
</head>
<body bgcolor="#f6f6f6"
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;">
<!-- body -->
<table class="body-wrap" bgcolor="#f6f6f6"
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;">
<tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
<td class="container" bgcolor="#FFFFFF"
style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;display:block!important;max-width:600px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;border-width:1px;border-style:solid;border-color:#f0f0f0;">
<!-- content -->
<div class="content"
style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;">
<table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
<tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
<h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;color:#444;margin-top:10px;margin-bottom:10px;margin-right:0;margin-left:0;line-height:1.2;font-weight:200;font-size:36px;">
Email Confirmation</h1>
<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
Thanks for joining <a href="{{ baseUrl('/') }}">{{ setting('app-name')}}</a>. <br/>
Please confirm your email address by clicking the button below.</p>
<table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
<tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
<td class="padding"
style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;padding-top:10px;padding-bottom:10px;padding-right:0;padding-left:0;">
<p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
<a class="btn-primary" href="{{ baseUrl('/register/confirm/' . $token) }}"
style="margin-top:0;margin-bottom:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;text-decoration:none;color:#FFF;background-color:#348eda;border-style:solid;border-color:#348eda;border-width:10px 20px;line-height:2;font-weight:bold;margin-right:10px;text-align:center;cursor:pointer;display:inline-block;border-radius:4px;">Confirm
Email</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!-- /content -->
</td>
<td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
</tr>
</table>
<!-- /body -->
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,7 @@
<div class="row"> <div class="row">
<div class="col-sm-8"> <div class="col-sm-8">
<div class="compact"> <div class="compact">
{!! $users->links() !!} {{ $users->links() }}
</div> </div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
@ -76,7 +76,7 @@
</table> </table>
<div> <div>
{!! $users->links() !!} {{ $users->links() }}
</div> </div>
</div> </div>

View File

View File

@ -0,0 +1,22 @@
<?php
if (! empty($greeting)) {
echo $greeting, "\n\n";
} else {
echo $level == 'error' ? 'Whoops!' : 'Hello!', "\n\n";
}
if (! empty($introLines)) {
echo implode("\n", $introLines), "\n\n";
}
if (isset($actionText)) {
echo "{$actionText}: {$actionUrl}", "\n\n";
}
if (! empty($outroLines)) {
echo implode("\n", $outroLines), "\n\n";
}
echo 'Regards,', "\n";
echo config('app.name'), "\n";

View File

@ -0,0 +1,206 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css" rel="stylesheet" media="all">
/* Media Queries */
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
@media only screen and (max-width: 600px) {
.button {
width: 100% !important;
}
.mobile {
max-width: 100%;
display: block;
width: 100%;
}
}
</style>
</head>
<?php
$style = [
/* Layout ------------------------------ */
'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;',
'email-wrapper' => 'width: 100%; margin: 0; padding: 0; background-color: #F2F4F6;',
/* Masthead ----------------------- */
'email-masthead' => 'padding: 25px 0; text-align: center;',
'email-masthead_name' => 'font-size: 24px; font-weight: 400; color: #2F3133; text-decoration: none; text-shadow: 0 1px 0 white;',
'email-body' => 'width: 100%; margin: 0; padding: 0; border-top: 4px solid '.setting('app-color').'; border-bottom: 1px solid #EDEFF2; background-color: #FFF;',
'email-body_inner' => 'width: auto; max-width: 100%; margin: 0 auto; padding: 0;',
'email-body_cell' => 'padding: 35px;',
'email-footer' => 'width: auto; max-width: 570px; margin: 0 auto; padding: 0; text-align: center;',
'email-footer_cell' => 'color: #AEAEAE; padding: 35px; text-align: center;',
/* Body ------------------------------ */
'body_action' => 'width: 100%; margin: 30px auto; padding: 0; text-align: center;',
'body_sub' => 'margin-top: 25px; padding-top: 25px; border-top: 1px solid #EDEFF2;',
/* Type ------------------------------ */
'anchor' => 'color: '.setting('app-color').';overflow-wrap: break-word;word-wrap: break-word;word-break: break-all;word-break:break-word;',
'header-1' => 'margin-top: 0; color: #2F3133; font-size: 19px; font-weight: bold; text-align: left;',
'paragraph' => 'margin-top: 0; color: #74787E; font-size: 16px; line-height: 1.5em;',
'paragraph-sub' => 'margin-top: 0; color: #74787E; font-size: 12px; line-height: 1.5em;',
'paragraph-center' => 'text-align: center;',
/* Buttons ------------------------------ */
'button' => 'display: block; display: inline-block; width: 200px; min-height: 20px; padding: 10px;
background-color: #3869D4; border-radius: 3px; color: #ffffff; font-size: 15px; line-height: 25px;
text-align: center; text-decoration: none; -webkit-text-size-adjust: none;',
'button--green' => 'background-color: #22BC66;',
'button--red' => 'background-color: #dc4d2f;',
'button--blue' => 'background-color: '.setting('app-color').';',
];
?>
<?php $fontFamily = 'font-family: Arial, \'Helvetica Neue\', Helvetica, sans-serif;'; ?>
<body style="{{ $style['body'] }}">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" class="mobile">
<table width="600" style="max-width: 100%; padding: 12px;text-align: left;" cellpadding="0" cellspacing="0" class="mobile">
<tr>
<td style="{{ $style['email-wrapper'] }}" align="center">
<table width="100%" cellpadding="0" cellspacing="0">
<!-- Logo -->
<tr>
<td style="{{ $style['email-masthead'] }}">
<a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ baseUrl('/') }}" target="_blank">
{{ setting('app-name') }}
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td style="{{ $style['email-body'] }}" width="100%">
<table style="{{ $style['email-body_inner'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="{{ $fontFamily }} {{ $style['email-body_cell'] }}">
<!-- Greeting -->
@if (!empty($greeting) || $level == 'error')
<h1 style="{{ $style['header-1'] }}">
@if (! empty($greeting))
{{ $greeting }}
@else
@if ($level == 'error')
Whoops!
@endif
@endif
</h1>
@endif
<!-- Intro -->
@foreach ($introLines as $line)
<p style="{{ $style['paragraph'] }}">
{{ $line }}
</p>
@endforeach
<!-- Action Button -->
@if (isset($actionText))
<table style="{{ $style['body_action'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<?php
switch ($level) {
case 'success':
$actionColor = 'button--green';
break;
case 'error':
$actionColor = 'button--red';
break;
default:
$actionColor = 'button--blue';
}
?>
<a href="{{ $actionUrl }}"
style="{{ $fontFamily }} {{ $style['button'] }} {{ $style[$actionColor] }}"
class="button"
target="_blank">
{{ $actionText }}
</a>
</td>
</tr>
</table>
@endif
<!-- Outro -->
@foreach ($outroLines as $line)
<p style="{{ $style['paragraph'] }}">
{{ $line }}
</p>
@endforeach
<!-- Sub Copy -->
@if (isset($actionText))
<table style="{{ $style['body_sub'] }}">
<tr>
<td style="{{ $fontFamily }}">
<p style="{{ $style['paragraph-sub'] }}">
If youre having trouble clicking the "{{ $actionText }}" button,
copy and paste the URL below into your web browser:
</p>
<p style="{{ $style['paragraph-sub'] }}">
<a style="{{ $style['anchor'] }}" href="{{ $actionUrl }}" target="_blank">
{{ $actionUrl }}
</a>
</p>
</td>
</tr>
</table>
@endif
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td>
<table style="{{ $style['email-footer'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="{{ $fontFamily }} {{ $style['email-footer_cell'] }}">
<p style="{{ $style['paragraph-sub'] }}">
&copy; {{ date('Y') }}
<a style="{{ $style['anchor'] }}" href="{{ baseUrl('/') }}" target="_blank">{{ setting('app-name') }}</a>.
All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -139,27 +139,27 @@ Route::group(['middleware' => 'auth'], function () {
}); });
// Login using social authentication // Social auth routes
Route::get('/login/service/{socialDriver}', 'Auth\AuthController@getSocialLogin'); Route::get('/login/service/{socialDriver}', 'Auth\RegisterController@getSocialLogin');
Route::get('/login/service/{socialDriver}/callback', 'Auth\AuthController@socialCallback'); Route::get('/login/service/{socialDriver}/callback', 'Auth\RegisterController@socialCallback');
Route::get('/login/service/{socialDriver}/detach', 'Auth\AuthController@detachSocialAccount'); Route::get('/login/service/{socialDriver}/detach', 'Auth\RegisterController@detachSocialAccount');
Route::get('/register/service/{socialDriver}', 'Auth\RegisterController@socialRegister');
// Login/Logout routes // Login/Logout routes
Route::get('/login', 'Auth\AuthController@getLogin'); Route::get('/login', 'Auth\LoginController@getLogin');
Route::post('/login', 'Auth\AuthController@postLogin'); Route::post('/login', 'Auth\LoginController@login');
Route::get('/logout', 'Auth\AuthController@getLogout'); Route::get('/logout', 'Auth\LoginController@logout');
Route::get('/register', 'Auth\AuthController@getRegister'); Route::get('/register', 'Auth\RegisterController@getRegister');
Route::get('/register/confirm', 'Auth\AuthController@getRegisterConfirmation'); Route::get('/register/confirm', 'Auth\RegisterController@getRegisterConfirmation');
Route::get('/register/confirm/awaiting', 'Auth\AuthController@showAwaitingConfirmation'); Route::get('/register/confirm/awaiting', 'Auth\RegisterController@showAwaitingConfirmation');
Route::post('/register/confirm/resend', 'Auth\AuthController@resendConfirmation'); Route::post('/register/confirm/resend', 'Auth\RegisterController@resendConfirmation');
Route::get('/register/confirm/{token}', 'Auth\AuthController@confirmEmail'); Route::get('/register/confirm/{token}', 'Auth\RegisterController@confirmEmail');
Route::get('/register/confirm/{token}/email', 'Auth\AuthController@viewConfirmEmail'); Route::post('/register', 'Auth\RegisterController@postRegister');
Route::get('/register/service/{socialDriver}', 'Auth\AuthController@socialRegister');
Route::post('/register', 'Auth\AuthController@postRegister');
// Password reset link request routes... // Password reset link request routes...
Route::get('/password/email', 'Auth\PasswordController@getEmail'); Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
Route::post('/password/email', 'Auth\PasswordController@postEmail'); Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
// Password reset routes... // Password reset routes...
Route::get('/password/reset/{token}', 'Auth\PasswordController@getReset'); Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
Route::post('/password/reset', 'Auth\PasswordController@postReset'); Route::post('/password/reset', 'Auth\ResetPasswordController@reset');

View File

@ -1,6 +1,7 @@
<?php <?php
use BookStack\EmailConfirmation; use BookStack\Notifications\ConfirmEmail;
use Illuminate\Support\Facades\Notification;
class AuthTest extends TestCase class AuthTest extends TestCase
{ {
@ -57,15 +58,13 @@ class AuthTest extends TestCase
public function test_confirmed_registration() public function test_confirmed_registration()
{ {
// Fake notifications
Notification::fake();
// Set settings and get user instance // Set settings and get user instance
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
$user = factory(\BookStack\User::class)->make(); $user = factory(\BookStack\User::class)->make();
// Mock Mailer to ensure mail is being sent
$mockMailer = Mockery::mock('Illuminate\Contracts\Mail\Mailer');
$mockMailer->shouldReceive('send')->with('emails/email-confirmation', Mockery::type('array'), Mockery::type('callable'))->twice();
$this->app->instance('mailer', $mockMailer);
// Go through registration process // Go through registration process
$this->visit('/register') $this->visit('/register')
->see('Sign Up') ->see('Sign Up')
@ -76,6 +75,10 @@ class AuthTest extends TestCase
->seePageIs('/register/confirm') ->seePageIs('/register/confirm')
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
// Ensure notification sent
$dbUser = \BookStack\User::where('email', '=', $user->email)->first();
Notification::assertSentTo($dbUser, ConfirmEmail::class);
// Test access and resend confirmation email // Test access and resend confirmation email
$this->login($user->email, $user->password) $this->login($user->email, $user->password)
->seePageIs('/register/confirm/awaiting') ->seePageIs('/register/confirm/awaiting')
@ -84,19 +87,18 @@ class AuthTest extends TestCase
->seePageIs('/register/confirm/awaiting') ->seePageIs('/register/confirm/awaiting')
->press('Resend Confirmation Email'); ->press('Resend Confirmation Email');
// Get confirmation // Get confirmation and confirm notification matches
$user = $user->where('email', '=', $user->email)->first(); $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
$emailConfirmation = EmailConfirmation::where('user_id', '=', $user->id)->first(); Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) {
return $notification->token === $emailConfirmation->token;
});
// Check confirmation email confirmation activation.
// Check confirmation email button and confirmation activation. $this->visit('/register/confirm/' . $emailConfirmation->token)
$this->visit('/register/confirm/' . $emailConfirmation->token . '/email')
->see('Email Confirmation')
->click('Confirm Email')
->seePageIs('/') ->seePageIs('/')
->see($user->name) ->see($user->name)
->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token]) ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token])
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]); ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
} }
public function test_restricted_registration() public function test_restricted_registration()

View File

@ -236,8 +236,9 @@ class EntityTest extends TestCase
->type('super test page', '#name') ->type('super test page', '#name')
->press('Save Page') ->press('Save Page')
// Check redirect // Check redirect
->seePageIs($newPageUrl) ->seePageIs($newPageUrl);
->visit($pageUrl)
$this->visit($pageUrl)
->seePageIs($newPageUrl); ->seePageIs($newPageUrl);
} }

View File

@ -59,8 +59,10 @@ class ImageTest extends TestCase
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists'); $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image exists');
$this->deleteImage($relPath);
$this->seeInDatabase('images', [ $this->seeInDatabase('images', [
'url' => $relPath, 'url' => $this->baseUrl . $relPath,
'type' => 'gallery', 'type' => 'gallery',
'uploaded_to' => $page->id, 'uploaded_to' => $page->id,
'path' => $relPath, 'path' => $relPath,
@ -69,7 +71,7 @@ class ImageTest extends TestCase
'name' => $imageName 'name' => $imageName
]); ]);
$this->deleteImage($relPath);
} }
public function test_image_delete() public function test_image_delete()
@ -85,7 +87,7 @@ class ImageTest extends TestCase
$this->assertResponseOk(); $this->assertResponseOk();
$this->dontSeeInDatabase('images', [ $this->dontSeeInDatabase('images', [
'url' => $relPath, 'url' => $this->baseUrl . $relPath,
'type' => 'gallery' 'type' => 'gallery'
]); ]);