Merge branch 'master' into release
This commit is contained in:
commit
4fc75beed4
|
@ -31,11 +31,7 @@ abstract class Entity extends Model
|
||||||
|
|
||||||
if ($matches) return true;
|
if ($matches) return true;
|
||||||
|
|
||||||
if ($entity->isA('chapter') && $this->isA('book')) {
|
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
||||||
return $entity->book_id === $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($entity->isA('page') && $this->isA('book')) {
|
|
||||||
return $entity->book_id === $this->id;
|
return $entity->book_id === $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,15 +60,6 @@ abstract class Entity extends Model
|
||||||
return $this->morphMany('BookStack\View', 'viewable');
|
return $this->morphMany('BookStack\View', 'viewable');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get just the views for the current user.
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function userViews()
|
|
||||||
{
|
|
||||||
return $this->views()->where('user_id', '=', auth()->user()->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows checking of the exact class, Used to check entity type.
|
* Allows checking of the exact class, Used to check entity type.
|
||||||
* Cleaner method for is_a.
|
* Cleaner method for is_a.
|
||||||
|
|
|
@ -42,6 +42,15 @@ abstract class Controller extends BaseController
|
||||||
$this->signedIn = auth()->check();
|
$this->signedIn = auth()->check();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the application and shows a permission error if
|
||||||
|
* the application is in demo mode.
|
||||||
|
*/
|
||||||
|
protected function preventAccessForDemoUsers()
|
||||||
|
{
|
||||||
|
if (env('APP_ENV', 'production') === 'demo') $this->showPermissionError();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the page title into the view.
|
* Adds the page title into the view.
|
||||||
* @param $title
|
* @param $title
|
||||||
|
@ -51,6 +60,18 @@ abstract class Controller extends BaseController
|
||||||
view()->share('pageTitle', $title);
|
view()->share('pageTitle', $title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On a permission error redirect to home and display
|
||||||
|
* the error as a notification.
|
||||||
|
*/
|
||||||
|
protected function showPermissionError()
|
||||||
|
{
|
||||||
|
Session::flash('error', trans('errors.permission'));
|
||||||
|
throw new HttpResponseException(
|
||||||
|
redirect('/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for a permission.
|
* Checks for a permission.
|
||||||
*
|
*
|
||||||
|
@ -60,15 +81,18 @@ abstract class Controller extends BaseController
|
||||||
protected function checkPermission($permissionName)
|
protected function checkPermission($permissionName)
|
||||||
{
|
{
|
||||||
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
|
||||||
Session::flash('error', trans('errors.permission'));
|
$this->showPermissionError();
|
||||||
throw new HttpResponseException(
|
|
||||||
redirect('/')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user has a permission or bypass if the callback is true.
|
||||||
|
* @param $permissionName
|
||||||
|
* @param $callback
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
protected function checkPermissionOr($permissionName, $callback)
|
protected function checkPermissionOr($permissionName, $callback)
|
||||||
{
|
{
|
||||||
$callbackResult = $callback();
|
$callbackResult = $callback();
|
||||||
|
|
|
@ -62,9 +62,9 @@ class SearchController extends Controller
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
$searchTerm = $request->get('term');
|
$searchTerm = $request->get('term');
|
||||||
$whereTerm = [['book_id', '=', $bookId]];
|
$searchWhereTerms = [['book_id', '=', $bookId]];
|
||||||
$pages = $this->pageRepo->getBySearch($searchTerm, $whereTerm);
|
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||||
$chapters = $this->chapterRepo->getBySearch($searchTerm, $whereTerm);
|
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
|
||||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,13 +31,16 @@ class SettingController extends Controller
|
||||||
*/
|
*/
|
||||||
public function update(Request $request)
|
public function update(Request $request)
|
||||||
{
|
{
|
||||||
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermission('settings-update');
|
$this->checkPermission('settings-update');
|
||||||
|
|
||||||
// Cycles through posted settings and update them
|
// Cycles through posted settings and update them
|
||||||
foreach($request->all() as $name => $value) {
|
foreach($request->all() as $name => $value) {
|
||||||
if(strpos($name, 'setting-') !== 0) continue;
|
if(strpos($name, 'setting-') !== 0) continue;
|
||||||
$key = str_replace('setting-', '', trim($name));
|
$key = str_replace('setting-', '', trim($name));
|
||||||
Setting::put($key, $value);
|
Setting::put($key, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->flash('success', 'Settings Saved');
|
session()->flash('success', 'Settings Saved');
|
||||||
return redirect('/settings');
|
return redirect('/settings');
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,15 +108,19 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermissionOr('user-update', function () use ($id) {
|
$this->checkPermissionOr('user-update', function () use ($id) {
|
||||||
return $this->currentUser->id == $id;
|
return $this->currentUser->id == $id;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required',
|
'name' => 'required',
|
||||||
'email' => 'required|email|unique:users,email,' . $id,
|
'email' => 'required|email|unique:users,email,' . $id,
|
||||||
'password' => 'min:5',
|
'password' => 'min:5|required_with:password_confirm',
|
||||||
'password-confirm' => 'same:password',
|
'password-confirm' => 'same:password|required_with:password',
|
||||||
'role' => 'exists:roles,id'
|
'role' => 'exists:roles,id'
|
||||||
|
], [
|
||||||
|
'password-confirm.required_with' => 'Password confirmation required'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = $this->user->findOrFail($id);
|
$user = $this->user->findOrFail($id);
|
||||||
|
@ -130,6 +134,7 @@ class UserController extends Controller
|
||||||
$password = $request->get('password');
|
$password = $request->get('password');
|
||||||
$user->password = bcrypt($password);
|
$user->password = bcrypt($password);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
return redirect('/users');
|
return redirect('/users');
|
||||||
}
|
}
|
||||||
|
@ -144,6 +149,7 @@ class UserController extends Controller
|
||||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||||
return $this->currentUser->id == $id;
|
return $this->currentUser->id == $id;
|
||||||
});
|
});
|
||||||
|
|
||||||
$user = $this->user->findOrFail($id);
|
$user = $this->user->findOrFail($id);
|
||||||
$this->setPageTitle('Delete User ' . $user->name);
|
$this->setPageTitle('Delete User ' . $user->name);
|
||||||
return view('users/delete', ['user' => $user]);
|
return view('users/delete', ['user' => $user]);
|
||||||
|
@ -156,6 +162,7 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermissionOr('user-delete', function () use ($id) {
|
$this->checkPermissionOr('user-delete', function () use ($id) {
|
||||||
return $this->currentUser->id == $id;
|
return $this->currentUser->id == $id;
|
||||||
});
|
});
|
||||||
|
|
12
app/Role.php
12
app/Role.php
|
@ -43,6 +43,16 @@ class Role extends Model
|
||||||
*/
|
*/
|
||||||
public static function getDefault()
|
public static function getDefault()
|
||||||
{
|
{
|
||||||
return static::where('name', '=', static::$default)->first();
|
return static::getRole(static::$default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the role object for the specified role.
|
||||||
|
* @param $roleName
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function getRole($roleName)
|
||||||
|
{
|
||||||
|
return static::where('name', '=', $roleName)->first();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ class ActivityService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out similar acitivity.
|
* Filters out similar activity.
|
||||||
* @param Activity[] $activity
|
* @param Activity[] $activity
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
|
||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
$user = factory(BookStack\User::class, 1)->create();
|
$user = factory(BookStack\User::class, 1)->create();
|
||||||
$role = \BookStack\Role::where('name', '=', 'admin')->first();
|
$role = \BookStack\Role::getDefault();
|
||||||
$user->attachRole($role);
|
$user->attachRole($role);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,6 @@
|
||||||
<env name="QUEUE_DRIVER" value="sync"/>
|
<env name="QUEUE_DRIVER" value="sync"/>
|
||||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||||
<env name="MAIL_PRETEND" value="true"/>
|
<env name="MAIL_PRETEND" value="true"/>
|
||||||
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
|
@ -82,7 +82,7 @@ BookStack is provided under the MIT License.
|
||||||
These are the great projects used to help build BookStack:
|
These are the great projects used to help build BookStack:
|
||||||
|
|
||||||
* [Laravel](http://laravel.com/)
|
* [Laravel](http://laravel.com/)
|
||||||
* [VueJS](http://vuejs.org/)
|
* [AngularJS](https://angularjs.org/)
|
||||||
* [jQuery](https://jquery.com/)
|
* [jQuery](https://jquery.com/)
|
||||||
* [TinyMCE](https://www.tinymce.com/)
|
* [TinyMCE](https://www.tinymce.com/)
|
||||||
* [highlight.js](https://highlightjs.org/)
|
* [highlight.js](https://highlightjs.org/)
|
||||||
|
|
|
@ -127,7 +127,7 @@ module.exports = function (ngApp) {
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
||||||
ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', function ($scope, $http, $attrs) {
|
ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
|
||||||
$scope.searching = false;
|
$scope.searching = false;
|
||||||
$scope.searchTerm = '';
|
$scope.searchTerm = '';
|
||||||
$scope.searchResults = '';
|
$scope.searchResults = '';
|
||||||
|
@ -141,7 +141,7 @@ module.exports = function (ngApp) {
|
||||||
var searchUrl = '/search/book/' + $attrs.bookId;
|
var searchUrl = '/search/book/' + $attrs.bookId;
|
||||||
searchUrl += '?term=' + encodeURIComponent(term);
|
searchUrl += '?term=' + encodeURIComponent(term);
|
||||||
$http.get(searchUrl).then((response) => {
|
$http.get(searchUrl).then((response) => {
|
||||||
$scope.searchResults = response.data;
|
$scope.searchResults = $sce.trustAsHtml(response.data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -43,14 +43,14 @@
|
||||||
<div class="float right">
|
<div class="float right">
|
||||||
<div class="links text-center">
|
<div class="links text-center">
|
||||||
<a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
|
<a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
|
||||||
@if($currentUser->can('settings-update'))
|
@if(isset($currentUser) && $currentUser->can('settings-update'))
|
||||||
<a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
|
<a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
|
||||||
@endif
|
@endif
|
||||||
@if(!$signedIn)
|
@if(!isset($signedIn) || !$signedIn)
|
||||||
<a href="/login"><i class="zmdi zmdi-sign-in"></i>Sign In</a>
|
<a href="/login"><i class="zmdi zmdi-sign-in"></i>Sign In</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if($signedIn)
|
@if(isset($signedIn) && $signedIn)
|
||||||
<div class="dropdown-container" dropdown>
|
<div class="dropdown-container" dropdown>
|
||||||
<span class="user-name" dropdown-toggle>
|
<span class="user-name" dropdown-toggle>
|
||||||
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
|
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Page Not Found</h1>
|
<h1 class="text-muted">Page Not Found</h1>
|
||||||
<p>The page you were looking for could not be found.</p>
|
<p>Sorry, The page you were looking for could not be found.</p>
|
||||||
|
<a href="/" class="button">Return To Home</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@stop
|
@stop
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="col-md-6"></div>
|
<div class="col-md-6"></div>
|
||||||
<div class="col-md-6 faded">
|
<div class="col-md-6 faded">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete user</a>
|
<a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -102,10 +102,10 @@ class AuthTest extends TestCase
|
||||||
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
|
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUserControl()
|
public function testUserCreation()
|
||||||
{
|
{
|
||||||
$user = factory(\BookStack\User::class)->make();
|
$user = factory(\BookStack\User::class)->make();
|
||||||
// Test creation
|
|
||||||
$this->asAdmin()
|
$this->asAdmin()
|
||||||
->visit('/users')
|
->visit('/users')
|
||||||
->click('Add new user')
|
->click('Add new user')
|
||||||
|
@ -118,9 +118,12 @@ class AuthTest extends TestCase
|
||||||
->seeInDatabase('users', $user->toArray())
|
->seeInDatabase('users', $user->toArray())
|
||||||
->seePageIs('/users')
|
->seePageIs('/users')
|
||||||
->see($user->name);
|
->see($user->name);
|
||||||
$user = $user->where('email', '=', $user->email)->first();
|
}
|
||||||
|
|
||||||
// Test editing
|
public function testUserUpdating()
|
||||||
|
{
|
||||||
|
$user = \BookStack\User::all()->last();
|
||||||
|
$password = $user->password;
|
||||||
$this->asAdmin()
|
$this->asAdmin()
|
||||||
->visit('/users')
|
->visit('/users')
|
||||||
->click($user->name)
|
->click($user->name)
|
||||||
|
@ -129,20 +132,58 @@ class AuthTest extends TestCase
|
||||||
->type('Barry Scott', '#name')
|
->type('Barry Scott', '#name')
|
||||||
->press('Save')
|
->press('Save')
|
||||||
->seePageIs('/users')
|
->seePageIs('/users')
|
||||||
->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott'])
|
->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
|
||||||
->notSeeInDatabase('users', ['name' => $user->name]);
|
->notSeeInDatabase('users', ['name' => $user->name]);
|
||||||
$user = $user->find($user->id);
|
}
|
||||||
|
|
||||||
|
public function testUserPasswordUpdate()
|
||||||
|
{
|
||||||
|
$user = \BookStack\User::all()->last();
|
||||||
|
$userProfilePage = '/users/' . $user->id;
|
||||||
|
$this->asAdmin()
|
||||||
|
->visit($userProfilePage)
|
||||||
|
->type('newpassword', '#password')
|
||||||
|
->press('Save')
|
||||||
|
->seePageIs($userProfilePage)
|
||||||
|
->see('Password confirmation required')
|
||||||
|
|
||||||
|
->type('newpassword', '#password')
|
||||||
|
->type('newpassword', '#password-confirm')
|
||||||
|
->press('Save')
|
||||||
|
->seePageIs('/users');
|
||||||
|
|
||||||
|
$userPassword = \BookStack\User::find($user->id)->password;
|
||||||
|
$this->assertTrue(Hash::check('newpassword', $userPassword));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUserDeletion()
|
||||||
|
{
|
||||||
|
$userDetails = factory(\BookStack\User::class)->make();
|
||||||
|
$user = $this->getNewUser($userDetails->toArray());
|
||||||
|
|
||||||
// Test Deletion
|
|
||||||
$this->asAdmin()
|
$this->asAdmin()
|
||||||
->visit('/users/' . $user->id)
|
->visit('/users/' . $user->id)
|
||||||
->click('Delete user')
|
->click('Delete User')
|
||||||
->see($user->name)
|
->see($user->name)
|
||||||
->press('Confirm')
|
->press('Confirm')
|
||||||
->seePageIs('/users')
|
->seePageIs('/users')
|
||||||
->notSeeInDatabase('users', ['name' => $user->name]);
|
->notSeeInDatabase('users', ['name' => $user->name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUserCannotBeDeletedIfLastAdmin()
|
||||||
|
{
|
||||||
|
$adminRole = \BookStack\Role::getRole('admin');
|
||||||
|
// Ensure we currently only have 1 admin user
|
||||||
|
$this->assertEquals(1, $adminRole->users()->count());
|
||||||
|
$user = $adminRole->users->first();
|
||||||
|
|
||||||
|
$this->asAdmin()->visit('/users/' . $user->id)
|
||||||
|
->click('Delete User')
|
||||||
|
->press('Confirm')
|
||||||
|
->seePageIs('/users/' . $user->id)
|
||||||
|
->see('You cannot delete the only admin');
|
||||||
|
}
|
||||||
|
|
||||||
public function testLogout()
|
public function testLogout()
|
||||||
{
|
{
|
||||||
$this->asAdmin()
|
$this->asAdmin()
|
||||||
|
|
|
@ -180,6 +180,37 @@ class EntityTest extends TestCase
|
||||||
->seeStatusCode(200);
|
->seeStatusCode(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEmptySearchRedirectsBack()
|
||||||
|
{
|
||||||
|
$this->asAdmin()
|
||||||
|
->visit('/')
|
||||||
|
->visit('/search/all')
|
||||||
|
->seePageIs('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBookSearch()
|
||||||
|
{
|
||||||
|
$book = \BookStack\Book::all()->first();
|
||||||
|
$page = $book->pages->last();
|
||||||
|
$chapter = $book->chapters->last();
|
||||||
|
|
||||||
|
$this->asAdmin()
|
||||||
|
->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
|
||||||
|
->see($page->name)
|
||||||
|
|
||||||
|
->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
|
||||||
|
->see($chapter->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyBookSearchRedirectsBack()
|
||||||
|
{
|
||||||
|
$book = \BookStack\Book::all()->first();
|
||||||
|
$this->asAdmin()
|
||||||
|
->visit('/books')
|
||||||
|
->visit('/search/book/' . $book->id . '?term=')
|
||||||
|
->seePageIs('/books');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testEntitiesViewableAfterCreatorDeletion()
|
public function testEntitiesViewableAfterCreatorDeletion()
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue