Dan Brown

Merge branch 'master' into release

...@@ -31,11 +31,7 @@ abstract class Entity extends Model ...@@ -31,11 +31,7 @@ abstract class Entity extends Model
31 31
32 if ($matches) return true; 32 if ($matches) return true;
33 33
34 - if ($entity->isA('chapter') && $this->isA('book')) { 34 + if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
35 - return $entity->book_id === $this->id;
36 - }
37 -
38 - if ($entity->isA('page') && $this->isA('book')) {
39 return $entity->book_id === $this->id; 35 return $entity->book_id === $this->id;
40 } 36 }
41 37
...@@ -65,15 +61,6 @@ abstract class Entity extends Model ...@@ -65,15 +61,6 @@ abstract class Entity extends Model
65 } 61 }
66 62
67 /** 63 /**
68 - * Get just the views for the current user.
69 - * @return mixed
70 - */
71 - public function userViews()
72 - {
73 - return $this->views()->where('user_id', '=', auth()->user()->id);
74 - }
75 -
76 - /**
77 * Allows checking of the exact class, Used to check entity type. 64 * Allows checking of the exact class, Used to check entity type.
78 * Cleaner method for is_a. 65 * Cleaner method for is_a.
79 * @param $type 66 * @param $type
......
...@@ -43,6 +43,15 @@ abstract class Controller extends BaseController ...@@ -43,6 +43,15 @@ abstract class Controller extends BaseController
43 } 43 }
44 44
45 /** 45 /**
46 + * Stops the application and shows a permission error if
47 + * the application is in demo mode.
48 + */
49 + protected function preventAccessForDemoUsers()
50 + {
51 + if (env('APP_ENV', 'production') === 'demo') $this->showPermissionError();
52 + }
53 +
54 + /**
46 * Adds the page title into the view. 55 * Adds the page title into the view.
47 * @param $title 56 * @param $title
48 */ 57 */
...@@ -52,6 +61,18 @@ abstract class Controller extends BaseController ...@@ -52,6 +61,18 @@ abstract class Controller extends BaseController
52 } 61 }
53 62
54 /** 63 /**
64 + * On a permission error redirect to home and display
65 + * the error as a notification.
66 + */
67 + protected function showPermissionError()
68 + {
69 + Session::flash('error', trans('errors.permission'));
70 + throw new HttpResponseException(
71 + redirect('/')
72 + );
73 + }
74 +
75 + /**
55 * Checks for a permission. 76 * Checks for a permission.
56 * 77 *
57 * @param $permissionName 78 * @param $permissionName
...@@ -60,15 +81,18 @@ abstract class Controller extends BaseController ...@@ -60,15 +81,18 @@ abstract class Controller extends BaseController
60 protected function checkPermission($permissionName) 81 protected function checkPermission($permissionName)
61 { 82 {
62 if (!$this->currentUser || !$this->currentUser->can($permissionName)) { 83 if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
63 - Session::flash('error', trans('errors.permission')); 84 + $this->showPermissionError();
64 - throw new HttpResponseException(
65 - redirect('/')
66 - );
67 } 85 }
68 86
69 return true; 87 return true;
70 } 88 }
71 89
90 + /**
91 + * Check if a user has a permission or bypass if the callback is true.
92 + * @param $permissionName
93 + * @param $callback
94 + * @return bool
95 + */
72 protected function checkPermissionOr($permissionName, $callback) 96 protected function checkPermissionOr($permissionName, $callback)
73 { 97 {
74 $callbackResult = $callback(); 98 $callbackResult = $callback();
......
...@@ -62,9 +62,9 @@ class SearchController extends Controller ...@@ -62,9 +62,9 @@ class SearchController extends Controller
62 return redirect()->back(); 62 return redirect()->back();
63 } 63 }
64 $searchTerm = $request->get('term'); 64 $searchTerm = $request->get('term');
65 - $whereTerm = [['book_id', '=', $bookId]]; 65 + $searchWhereTerms = [['book_id', '=', $bookId]];
66 - $pages = $this->pageRepo->getBySearch($searchTerm, $whereTerm); 66 + $pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms);
67 - $chapters = $this->chapterRepo->getBySearch($searchTerm, $whereTerm); 67 + $chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms);
68 return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); 68 return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
69 } 69 }
70 70
......
...@@ -31,13 +31,16 @@ class SettingController extends Controller ...@@ -31,13 +31,16 @@ class SettingController extends Controller
31 */ 31 */
32 public function update(Request $request) 32 public function update(Request $request)
33 { 33 {
34 + $this->preventAccessForDemoUsers();
34 $this->checkPermission('settings-update'); 35 $this->checkPermission('settings-update');
36 +
35 // Cycles through posted settings and update them 37 // Cycles through posted settings and update them
36 foreach($request->all() as $name => $value) { 38 foreach($request->all() as $name => $value) {
37 if(strpos($name, 'setting-') !== 0) continue; 39 if(strpos($name, 'setting-') !== 0) continue;
38 $key = str_replace('setting-', '', trim($name)); 40 $key = str_replace('setting-', '', trim($name));
39 Setting::put($key, $value); 41 Setting::put($key, $value);
40 } 42 }
43 +
41 session()->flash('success', 'Settings Saved'); 44 session()->flash('success', 'Settings Saved');
42 return redirect('/settings'); 45 return redirect('/settings');
43 } 46 }
......
...@@ -108,15 +108,19 @@ class UserController extends Controller ...@@ -108,15 +108,19 @@ class UserController extends Controller
108 */ 108 */
109 public function update(Request $request, $id) 109 public function update(Request $request, $id)
110 { 110 {
111 + $this->preventAccessForDemoUsers();
111 $this->checkPermissionOr('user-update', function () use ($id) { 112 $this->checkPermissionOr('user-update', function () use ($id) {
112 return $this->currentUser->id == $id; 113 return $this->currentUser->id == $id;
113 }); 114 });
115 +
114 $this->validate($request, [ 116 $this->validate($request, [
115 'name' => 'required', 117 'name' => 'required',
116 'email' => 'required|email|unique:users,email,' . $id, 118 'email' => 'required|email|unique:users,email,' . $id,
117 - 'password' => 'min:5', 119 + 'password' => 'min:5|required_with:password_confirm',
118 - 'password-confirm' => 'same:password', 120 + 'password-confirm' => 'same:password|required_with:password',
119 'role' => 'exists:roles,id' 121 'role' => 'exists:roles,id'
122 + ], [
123 + 'password-confirm.required_with' => 'Password confirmation required'
120 ]); 124 ]);
121 125
122 $user = $this->user->findOrFail($id); 126 $user = $this->user->findOrFail($id);
...@@ -130,6 +134,7 @@ class UserController extends Controller ...@@ -130,6 +134,7 @@ class UserController extends Controller
130 $password = $request->get('password'); 134 $password = $request->get('password');
131 $user->password = bcrypt($password); 135 $user->password = bcrypt($password);
132 } 136 }
137 +
133 $user->save(); 138 $user->save();
134 return redirect('/users'); 139 return redirect('/users');
135 } 140 }
...@@ -144,6 +149,7 @@ class UserController extends Controller ...@@ -144,6 +149,7 @@ class UserController extends Controller
144 $this->checkPermissionOr('user-delete', function () use ($id) { 149 $this->checkPermissionOr('user-delete', function () use ($id) {
145 return $this->currentUser->id == $id; 150 return $this->currentUser->id == $id;
146 }); 151 });
152 +
147 $user = $this->user->findOrFail($id); 153 $user = $this->user->findOrFail($id);
148 $this->setPageTitle('Delete User ' . $user->name); 154 $this->setPageTitle('Delete User ' . $user->name);
149 return view('users/delete', ['user' => $user]); 155 return view('users/delete', ['user' => $user]);
...@@ -156,6 +162,7 @@ class UserController extends Controller ...@@ -156,6 +162,7 @@ class UserController extends Controller
156 */ 162 */
157 public function destroy($id) 163 public function destroy($id)
158 { 164 {
165 + $this->preventAccessForDemoUsers();
159 $this->checkPermissionOr('user-delete', function () use ($id) { 166 $this->checkPermissionOr('user-delete', function () use ($id) {
160 return $this->currentUser->id == $id; 167 return $this->currentUser->id == $id;
161 }); 168 });
......
...@@ -43,6 +43,16 @@ class Role extends Model ...@@ -43,6 +43,16 @@ class Role extends Model
43 */ 43 */
44 public static function getDefault() 44 public static function getDefault()
45 { 45 {
46 - return static::where('name', '=', static::$default)->first(); 46 + return static::getRole(static::$default);
47 + }
48 +
49 + /**
50 + * Get the role object for the specified role.
51 + * @param $roleName
52 + * @return mixed
53 + */
54 + public static function getRole($roleName)
55 + {
56 + return static::where('name', '=', $roleName)->first();
47 } 57 }
48 } 58 }
......
...@@ -107,7 +107,7 @@ class ActivityService ...@@ -107,7 +107,7 @@ class ActivityService
107 } 107 }
108 108
109 /** 109 /**
110 - * Filters out similar acitivity. 110 + * Filters out similar activity.
111 * @param Activity[] $activity 111 * @param Activity[] $activity
112 * @return array 112 * @return array
113 */ 113 */
......
...@@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder ...@@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
12 public function run() 12 public function run()
13 { 13 {
14 $user = factory(BookStack\User::class, 1)->create(); 14 $user = factory(BookStack\User::class, 1)->create();
15 - $role = \BookStack\Role::where('name', '=', 'admin')->first(); 15 + $role = \BookStack\Role::getDefault();
16 $user->attachRole($role); 16 $user->attachRole($role);
17 17
18 18
......
...@@ -26,6 +26,6 @@ ...@@ -26,6 +26,6 @@
26 <env name="QUEUE_DRIVER" value="sync"/> 26 <env name="QUEUE_DRIVER" value="sync"/>
27 <env name="DB_CONNECTION" value="mysql_testing"/> 27 <env name="DB_CONNECTION" value="mysql_testing"/>
28 <env name="MAIL_PRETEND" value="true"/> 28 <env name="MAIL_PRETEND" value="true"/>
29 - <env name="DISABLE_EXTERNAL_SERVICES" value="true"/> 29 + <env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
30 </php> 30 </php>
31 </phpunit> 31 </phpunit>
......
1 +*
2 +!.gitignore
...@@ -82,7 +82,7 @@ BookStack is provided under the MIT License. ...@@ -82,7 +82,7 @@ BookStack is provided under the MIT License.
82 These are the great projects used to help build BookStack: 82 These are the great projects used to help build BookStack:
83 83
84 * [Laravel](http://laravel.com/) 84 * [Laravel](http://laravel.com/)
85 -* [VueJS](http://vuejs.org/) 85 +* [AngularJS](https://angularjs.org/)
86 * [jQuery](https://jquery.com/) 86 * [jQuery](https://jquery.com/)
87 * [TinyMCE](https://www.tinymce.com/) 87 * [TinyMCE](https://www.tinymce.com/)
88 * [highlight.js](https://highlightjs.org/) 88 * [highlight.js](https://highlightjs.org/)
......
...@@ -127,7 +127,7 @@ module.exports = function (ngApp) { ...@@ -127,7 +127,7 @@ module.exports = function (ngApp) {
127 }]); 127 }]);
128 128
129 129
130 - ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', function ($scope, $http, $attrs) { 130 + ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
131 $scope.searching = false; 131 $scope.searching = false;
132 $scope.searchTerm = ''; 132 $scope.searchTerm = '';
133 $scope.searchResults = ''; 133 $scope.searchResults = '';
...@@ -141,7 +141,7 @@ module.exports = function (ngApp) { ...@@ -141,7 +141,7 @@ module.exports = function (ngApp) {
141 var searchUrl = '/search/book/' + $attrs.bookId; 141 var searchUrl = '/search/book/' + $attrs.bookId;
142 searchUrl += '?term=' + encodeURIComponent(term); 142 searchUrl += '?term=' + encodeURIComponent(term);
143 $http.get(searchUrl).then((response) => { 143 $http.get(searchUrl).then((response) => {
144 - $scope.searchResults = response.data; 144 + $scope.searchResults = $sce.trustAsHtml(response.data);
145 }); 145 });
146 }; 146 };
147 147
......
...@@ -43,14 +43,14 @@ ...@@ -43,14 +43,14 @@
43 <div class="float right"> 43 <div class="float right">
44 <div class="links text-center"> 44 <div class="links text-center">
45 <a href="/books"><i class="zmdi zmdi-book"></i>Books</a> 45 <a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
46 - @if($currentUser->can('settings-update')) 46 + @if(isset($currentUser) && $currentUser->can('settings-update'))
47 <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a> 47 <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
48 @endif 48 @endif
49 - @if(!$signedIn) 49 + @if(!isset($signedIn) || !$signedIn)
50 <a href="/login"><i class="zmdi zmdi-sign-in"></i>Sign In</a> 50 <a href="/login"><i class="zmdi zmdi-sign-in"></i>Sign In</a>
51 @endif 51 @endif
52 </div> 52 </div>
53 - @if($signedIn) 53 + @if(isset($signedIn) && $signedIn)
54 <div class="dropdown-container" dropdown> 54 <div class="dropdown-container" dropdown>
55 <span class="user-name" dropdown-toggle> 55 <span class="user-name" dropdown-toggle>
56 <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}"> 56 <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
......
...@@ -4,8 +4,9 @@ ...@@ -4,8 +4,9 @@
4 4
5 5
6 <div class="container"> 6 <div class="container">
7 - <h1>Page Not Found</h1> 7 + <h1 class="text-muted">Page Not Found</h1>
8 - <p>The page you were looking for could not be found.</p> 8 + <p>Sorry, The page you were looking for could not be found.</p>
9 + <a href="/" class="button">Return To Home</a>
9 </div> 10 </div>
10 11
11 @stop 12 @stop
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
9 <div class="col-md-6"></div> 9 <div class="col-md-6"></div>
10 <div class="col-md-6 faded"> 10 <div class="col-md-6 faded">
11 <div class="action-buttons"> 11 <div class="action-buttons">
12 - <a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete user</a> 12 + <a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
13 </div> 13 </div>
14 </div> 14 </div>
15 </div> 15 </div>
......
...@@ -102,10 +102,10 @@ class AuthTest extends TestCase ...@@ -102,10 +102,10 @@ class AuthTest extends TestCase
102 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]); 102 ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
103 } 103 }
104 104
105 - public function testUserControl() 105 + public function testUserCreation()
106 { 106 {
107 $user = factory(\BookStack\User::class)->make(); 107 $user = factory(\BookStack\User::class)->make();
108 - // Test creation 108 +
109 $this->asAdmin() 109 $this->asAdmin()
110 ->visit('/users') 110 ->visit('/users')
111 ->click('Add new user') 111 ->click('Add new user')
...@@ -118,9 +118,12 @@ class AuthTest extends TestCase ...@@ -118,9 +118,12 @@ class AuthTest extends TestCase
118 ->seeInDatabase('users', $user->toArray()) 118 ->seeInDatabase('users', $user->toArray())
119 ->seePageIs('/users') 119 ->seePageIs('/users')
120 ->see($user->name); 120 ->see($user->name);
121 - $user = $user->where('email', '=', $user->email)->first(); 121 + }
122 122
123 - // Test editing 123 + public function testUserUpdating()
124 + {
125 + $user = \BookStack\User::all()->last();
126 + $password = $user->password;
124 $this->asAdmin() 127 $this->asAdmin()
125 ->visit('/users') 128 ->visit('/users')
126 ->click($user->name) 129 ->click($user->name)
...@@ -129,20 +132,58 @@ class AuthTest extends TestCase ...@@ -129,20 +132,58 @@ class AuthTest extends TestCase
129 ->type('Barry Scott', '#name') 132 ->type('Barry Scott', '#name')
130 ->press('Save') 133 ->press('Save')
131 ->seePageIs('/users') 134 ->seePageIs('/users')
132 - ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott']) 135 + ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password])
133 ->notSeeInDatabase('users', ['name' => $user->name]); 136 ->notSeeInDatabase('users', ['name' => $user->name]);
134 - $user = $user->find($user->id); 137 + }
138 +
139 + public function testUserPasswordUpdate()
140 + {
141 + $user = \BookStack\User::all()->last();
142 + $userProfilePage = '/users/' . $user->id;
143 + $this->asAdmin()
144 + ->visit($userProfilePage)
145 + ->type('newpassword', '#password')
146 + ->press('Save')
147 + ->seePageIs($userProfilePage)
148 + ->see('Password confirmation required')
149 +
150 + ->type('newpassword', '#password')
151 + ->type('newpassword', '#password-confirm')
152 + ->press('Save')
153 + ->seePageIs('/users');
154 +
155 + $userPassword = \BookStack\User::find($user->id)->password;
156 + $this->assertTrue(Hash::check('newpassword', $userPassword));
157 + }
158 +
159 + public function testUserDeletion()
160 + {
161 + $userDetails = factory(\BookStack\User::class)->make();
162 + $user = $this->getNewUser($userDetails->toArray());
135 163
136 - // Test Deletion
137 $this->asAdmin() 164 $this->asAdmin()
138 ->visit('/users/' . $user->id) 165 ->visit('/users/' . $user->id)
139 - ->click('Delete user') 166 + ->click('Delete User')
140 ->see($user->name) 167 ->see($user->name)
141 ->press('Confirm') 168 ->press('Confirm')
142 ->seePageIs('/users') 169 ->seePageIs('/users')
143 ->notSeeInDatabase('users', ['name' => $user->name]); 170 ->notSeeInDatabase('users', ['name' => $user->name]);
144 } 171 }
145 172
173 + public function testUserCannotBeDeletedIfLastAdmin()
174 + {
175 + $adminRole = \BookStack\Role::getRole('admin');
176 + // Ensure we currently only have 1 admin user
177 + $this->assertEquals(1, $adminRole->users()->count());
178 + $user = $adminRole->users->first();
179 +
180 + $this->asAdmin()->visit('/users/' . $user->id)
181 + ->click('Delete User')
182 + ->press('Confirm')
183 + ->seePageIs('/users/' . $user->id)
184 + ->see('You cannot delete the only admin');
185 + }
186 +
146 public function testLogout() 187 public function testLogout()
147 { 188 {
148 $this->asAdmin() 189 $this->asAdmin()
......
...@@ -180,6 +180,37 @@ class EntityTest extends TestCase ...@@ -180,6 +180,37 @@ class EntityTest extends TestCase
180 ->seeStatusCode(200); 180 ->seeStatusCode(200);
181 } 181 }
182 182
183 + public function testEmptySearchRedirectsBack()
184 + {
185 + $this->asAdmin()
186 + ->visit('/')
187 + ->visit('/search/all')
188 + ->seePageIs('/');
189 + }
190 +
191 + public function testBookSearch()
192 + {
193 + $book = \BookStack\Book::all()->first();
194 + $page = $book->pages->last();
195 + $chapter = $book->chapters->last();
196 +
197 + $this->asAdmin()
198 + ->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
199 + ->see($page->name)
200 +
201 + ->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
202 + ->see($chapter->name);
203 + }
204 +
205 + public function testEmptyBookSearchRedirectsBack()
206 + {
207 + $book = \BookStack\Book::all()->first();
208 + $this->asAdmin()
209 + ->visit('/books')
210 + ->visit('/search/book/' . $book->id . '?term=')
211 + ->seePageIs('/books');
212 + }
213 +
183 214
184 public function testEntitiesViewableAfterCreatorDeletion() 215 public function testEntitiesViewableAfterCreatorDeletion()
185 { 216 {
......