Dan Brown

Merge branch 'master' into nwalke-update_site_color

Showing 74 changed files with 2863 additions and 344 deletions
...@@ -12,8 +12,17 @@ DB_PASSWORD=database_user_password ...@@ -12,8 +12,17 @@ DB_PASSWORD=database_user_password
12 # Cache and session 12 # Cache and session
13 CACHE_DRIVER=file 13 CACHE_DRIVER=file
14 SESSION_DRIVER=file 14 SESSION_DRIVER=file
15 +# If using Memcached, comment the above and uncomment these
16 +#CACHE_DRIVER=memcached
17 +#SESSION_DRIVER=memcached
15 QUEUE_DRIVER=sync 18 QUEUE_DRIVER=sync
16 19
20 +# Memcached settings
21 +# If using a UNIX socket path for the host, set the port to 0
22 +# This follows the following format: HOST:PORT:WEIGHT
23 +# For multiple servers separate with a comma
24 +MEMCACHED_SERVERS=127.0.0.1:11211:100
25 +
17 # Storage 26 # Storage
18 STORAGE_TYPE=local 27 STORAGE_TYPE=local
19 # Amazon S3 Config 28 # Amazon S3 Config
...@@ -53,4 +62,4 @@ MAIL_HOST=localhost ...@@ -53,4 +62,4 @@ MAIL_HOST=localhost
53 MAIL_PORT=1025 62 MAIL_PORT=1025
54 MAIL_USERNAME=null 63 MAIL_USERNAME=null
55 MAIL_PASSWORD=null 64 MAIL_PASSWORD=null
56 -MAIL_ENCRYPTION=null 65 +MAIL_ENCRYPTION=null
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -15,15 +15,11 @@ class Activity extends Model ...@@ -15,15 +15,11 @@ class Activity extends Model
15 15
16 /** 16 /**
17 * Get the entity for this activity. 17 * Get the entity for this activity.
18 - * @return bool
19 */ 18 */
20 public function entity() 19 public function entity()
21 { 20 {
22 - if ($this->entity_id) { 21 + if ($this->entity_type === '') $this->entity_type = null;
23 - return $this->morphTo('entity')->first(); 22 + return $this->morphTo('entity');
24 - } else {
25 - return false;
26 - }
27 } 23 }
28 24
29 /** 25 /**
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 3
5 -use Illuminate\Database\Eloquent\Model; 4 +abstract class Entity extends Ownable
6 -
7 -abstract class Entity extends Model
8 { 5 {
9 6
10 - use Ownable;
11 -
12 /** 7 /**
13 * Compares this entity to another given entity. 8 * Compares this entity to another given entity.
14 * Matches by comparing class and id. 9 * Matches by comparing class and id.
...@@ -53,7 +48,6 @@ abstract class Entity extends Model ...@@ -53,7 +48,6 @@ abstract class Entity extends Model
53 48
54 /** 49 /**
55 * Get View objects for this entity. 50 * Get View objects for this entity.
56 - * @return mixed
57 */ 51 */
58 public function views() 52 public function views()
59 { 53 {
...@@ -61,34 +55,44 @@ abstract class Entity extends Model ...@@ -61,34 +55,44 @@ abstract class Entity extends Model
61 } 55 }
62 56
63 /** 57 /**
64 - * Allows checking of the exact class, Used to check entity type. 58 + * Get this entities restrictions.
65 - * Cleaner method for is_a. 59 + */
66 - * @param $type 60 + public function restrictions()
61 + {
62 + return $this->morphMany('BookStack\Restriction', 'restrictable');
63 + }
64 +
65 + /**
66 + * Check if this entity has a specific restriction set against it.
67 + * @param $role_id
68 + * @param $action
67 * @return bool 69 * @return bool
68 */ 70 */
69 - public static function isA($type) 71 + public function hasRestriction($role_id, $action)
70 { 72 {
71 - return static::getClassName() === strtolower($type); 73 + return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
72 } 74 }
73 75
74 /** 76 /**
75 - * Gets the class name. 77 + * Allows checking of the exact class, Used to check entity type.
76 - * @return string 78 + * Cleaner method for is_a.
79 + * @param $type
80 + * @return bool
77 */ 81 */
78 - public static function getClassName() 82 + public static function isA($type)
79 { 83 {
80 - return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]); 84 + return static::getClassName() === strtolower($type);
81 } 85 }
82 86
83 /** 87 /**
84 - *Gets a limited-length version of the entities name. 88 + * Gets a limited-length version of the entities name.
85 * @param int $length 89 * @param int $length
86 * @return string 90 * @return string
87 */ 91 */
88 public function getShortName($length = 25) 92 public function getShortName($length = 25)
89 { 93 {
90 - if(strlen($this->name) <= $length) return $this->name; 94 + if (strlen($this->name) <= $length) return $this->name;
91 - return substr($this->name, 0, $length-3) . '...'; 95 + return substr($this->name, 0, $length - 3) . '...';
92 } 96 }
93 97
94 /** 98 /**
...@@ -100,22 +104,40 @@ abstract class Entity extends Model ...@@ -100,22 +104,40 @@ abstract class Entity extends Model
100 */ 104 */
101 public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) 105 public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
102 { 106 {
103 - $termString = ''; 107 + $exactTerms = [];
104 - foreach ($terms as $term) { 108 + foreach ($terms as $key => $term) {
105 - $termString .= htmlentities($term) . '* '; 109 + $term = htmlentities($term, ENT_QUOTES);
110 + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
111 + if (preg_match('/\s/', $term)) {
112 + $exactTerms[] = '%' . $term . '%';
113 + $term = '"' . $term . '"';
114 + } else {
115 + $term = '' . $term . '*';
116 + }
117 + if ($term !== '*') $terms[$key] = $term;
106 } 118 }
119 + $termString = implode(' ', $terms);
107 $fields = implode(',', $fieldsToSearch); 120 $fields = implode(',', $fieldsToSearch);
108 - $termStringEscaped = \DB::connection()->getPdo()->quote($termString); 121 + $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
109 - $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
110 $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); 122 $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
111 123
124 + // Ensure at least one exact term matches if in search
125 + if (count($exactTerms) > 0) {
126 + $search = $search->where(function($query) use ($exactTerms, $fieldsToSearch) {
127 + foreach ($exactTerms as $exactTerm) {
128 + foreach ($fieldsToSearch as $field) {
129 + $query->orWhere($field, 'like', $exactTerm);
130 + }
131 + }
132 + });
133 + }
134 +
112 // Add additional where terms 135 // Add additional where terms
113 foreach ($wheres as $whereTerm) { 136 foreach ($wheres as $whereTerm) {
114 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); 137 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
115 } 138 }
116 -
117 // Load in relations 139 // Load in relations
118 - if (static::isA('page')) { 140 + if (static::isA('page')) {
119 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); 141 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
120 } else if (static::isA('chapter')) { 142 } else if (static::isA('chapter')) {
121 $search = $search->with('book'); 143 $search = $search->with('book');
......
...@@ -56,7 +56,8 @@ class Handler extends ExceptionHandler ...@@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
56 // Which will include the basic message to point the user roughly to the cause. 56 // Which will include the basic message to point the user roughly to the cause.
57 if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) { 57 if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
58 $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); 58 $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
59 - return response()->view('errors/500', ['message' => $message], 500); 59 + $code = ($e->getCode() === 0) ? 500 : $e->getCode();
60 + return response()->view('errors/' . $code, ['message' => $message], $code);
60 } 61 }
61 62
62 return parent::render($request, $e); 63 return parent::render($request, $e);
......
1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +class NotFoundException extends PrettyException {
5 +
6 + /**
7 + * NotFoundException constructor.
8 + * @param string $message
9 + */
10 + public function __construct($message = 'Item not found')
11 + {
12 + parent::__construct($message, 404);
13 + }
14 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +use Exception;
5 +
6 +class PermissionsException extends Exception {}
...\ No newline at end of file ...\ No newline at end of file
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 use Activity; 5 use Activity;
6 +use BookStack\Repos\UserRepo;
6 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
7 8
8 use Illuminate\Support\Facades\Auth; 9 use Illuminate\Support\Facades\Auth;
...@@ -19,18 +20,21 @@ class BookController extends Controller ...@@ -19,18 +20,21 @@ class BookController extends Controller
19 protected $bookRepo; 20 protected $bookRepo;
20 protected $pageRepo; 21 protected $pageRepo;
21 protected $chapterRepo; 22 protected $chapterRepo;
23 + protected $userRepo;
22 24
23 /** 25 /**
24 * BookController constructor. 26 * BookController constructor.
25 - * @param BookRepo $bookRepo 27 + * @param BookRepo $bookRepo
26 - * @param PageRepo $pageRepo 28 + * @param PageRepo $pageRepo
27 * @param ChapterRepo $chapterRepo 29 * @param ChapterRepo $chapterRepo
30 + * @param UserRepo $userRepo
28 */ 31 */
29 - public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo) 32 + public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
30 { 33 {
31 $this->bookRepo = $bookRepo; 34 $this->bookRepo = $bookRepo;
32 $this->pageRepo = $pageRepo; 35 $this->pageRepo = $pageRepo;
33 $this->chapterRepo = $chapterRepo; 36 $this->chapterRepo = $chapterRepo;
37 + $this->userRepo = $userRepo;
34 parent::__construct(); 38 parent::__construct();
35 } 39 }
36 40
...@@ -55,7 +59,7 @@ class BookController extends Controller ...@@ -55,7 +59,7 @@ class BookController extends Controller
55 */ 59 */
56 public function create() 60 public function create()
57 { 61 {
58 - $this->checkPermission('book-create'); 62 + $this->checkPermission('book-create-all');
59 $this->setPageTitle('Create New Book'); 63 $this->setPageTitle('Create New Book');
60 return view('books/create'); 64 return view('books/create');
61 } 65 }
...@@ -68,9 +72,9 @@ class BookController extends Controller ...@@ -68,9 +72,9 @@ class BookController extends Controller
68 */ 72 */
69 public function store(Request $request) 73 public function store(Request $request)
70 { 74 {
71 - $this->checkPermission('book-create'); 75 + $this->checkPermission('book-create-all');
72 $this->validate($request, [ 76 $this->validate($request, [
73 - 'name' => 'required|string|max:255', 77 + 'name' => 'required|string|max:255',
74 'description' => 'string|max:1000' 78 'description' => 'string|max:1000'
75 ]); 79 ]);
76 $book = $this->bookRepo->newFromInput($request->all()); 80 $book = $this->bookRepo->newFromInput($request->all());
...@@ -105,8 +109,8 @@ class BookController extends Controller ...@@ -105,8 +109,8 @@ class BookController extends Controller
105 */ 109 */
106 public function edit($slug) 110 public function edit($slug)
107 { 111 {
108 - $this->checkPermission('book-update');
109 $book = $this->bookRepo->getBySlug($slug); 112 $book = $this->bookRepo->getBySlug($slug);
113 + $this->checkOwnablePermission('book-update', $book);
110 $this->setPageTitle('Edit Book ' . $book->getShortName()); 114 $this->setPageTitle('Edit Book ' . $book->getShortName());
111 return view('books/edit', ['book' => $book, 'current' => $book]); 115 return view('books/edit', ['book' => $book, 'current' => $book]);
112 } 116 }
...@@ -120,10 +124,10 @@ class BookController extends Controller ...@@ -120,10 +124,10 @@ class BookController extends Controller
120 */ 124 */
121 public function update(Request $request, $slug) 125 public function update(Request $request, $slug)
122 { 126 {
123 - $this->checkPermission('book-update');
124 $book = $this->bookRepo->getBySlug($slug); 127 $book = $this->bookRepo->getBySlug($slug);
128 + $this->checkOwnablePermission('book-update', $book);
125 $this->validate($request, [ 129 $this->validate($request, [
126 - 'name' => 'required|string|max:255', 130 + 'name' => 'required|string|max:255',
127 'description' => 'string|max:1000' 131 'description' => 'string|max:1000'
128 ]); 132 ]);
129 $book->fill($request->all()); 133 $book->fill($request->all());
...@@ -141,8 +145,8 @@ class BookController extends Controller ...@@ -141,8 +145,8 @@ class BookController extends Controller
141 */ 145 */
142 public function showDelete($bookSlug) 146 public function showDelete($bookSlug)
143 { 147 {
144 - $this->checkPermission('book-delete');
145 $book = $this->bookRepo->getBySlug($bookSlug); 148 $book = $this->bookRepo->getBySlug($bookSlug);
149 + $this->checkOwnablePermission('book-delete', $book);
146 $this->setPageTitle('Delete Book ' . $book->getShortName()); 150 $this->setPageTitle('Delete Book ' . $book->getShortName());
147 return view('books/delete', ['book' => $book, 'current' => $book]); 151 return view('books/delete', ['book' => $book, 'current' => $book]);
148 } 152 }
...@@ -154,8 +158,8 @@ class BookController extends Controller ...@@ -154,8 +158,8 @@ class BookController extends Controller
154 */ 158 */
155 public function sort($bookSlug) 159 public function sort($bookSlug)
156 { 160 {
157 - $this->checkPermission('book-update');
158 $book = $this->bookRepo->getBySlug($bookSlug); 161 $book = $this->bookRepo->getBySlug($bookSlug);
162 + $this->checkOwnablePermission('book-update', $book);
159 $bookChildren = $this->bookRepo->getChildren($book); 163 $bookChildren = $this->bookRepo->getChildren($book);
160 $books = $this->bookRepo->getAll(false); 164 $books = $this->bookRepo->getAll(false);
161 $this->setPageTitle('Sort Book ' . $book->getShortName()); 165 $this->setPageTitle('Sort Book ' . $book->getShortName());
...@@ -177,15 +181,14 @@ class BookController extends Controller ...@@ -177,15 +181,14 @@ class BookController extends Controller
177 181
178 /** 182 /**
179 * Saves an array of sort mapping to pages and chapters. 183 * Saves an array of sort mapping to pages and chapters.
180 - *
181 * @param string $bookSlug 184 * @param string $bookSlug
182 * @param Request $request 185 * @param Request $request
183 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector 186 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
184 */ 187 */
185 public function saveSort($bookSlug, Request $request) 188 public function saveSort($bookSlug, Request $request)
186 { 189 {
187 - $this->checkPermission('book-update');
188 $book = $this->bookRepo->getBySlug($bookSlug); 190 $book = $this->bookRepo->getBySlug($bookSlug);
191 + $this->checkOwnablePermission('book-update', $book);
189 192
190 // Return if no map sent 193 // Return if no map sent
191 if (!$request->has('sort-tree')) { 194 if (!$request->has('sort-tree')) {
...@@ -223,17 +226,48 @@ class BookController extends Controller ...@@ -223,17 +226,48 @@ class BookController extends Controller
223 226
224 /** 227 /**
225 * Remove the specified book from storage. 228 * Remove the specified book from storage.
226 - *
227 * @param $bookSlug 229 * @param $bookSlug
228 * @return Response 230 * @return Response
229 */ 231 */
230 public function destroy($bookSlug) 232 public function destroy($bookSlug)
231 { 233 {
232 - $this->checkPermission('book-delete');
233 $book = $this->bookRepo->getBySlug($bookSlug); 234 $book = $this->bookRepo->getBySlug($bookSlug);
235 + $this->checkOwnablePermission('book-delete', $book);
234 Activity::addMessage('book_delete', 0, $book->name); 236 Activity::addMessage('book_delete', 0, $book->name);
235 Activity::removeEntity($book); 237 Activity::removeEntity($book);
236 $this->bookRepo->destroyBySlug($bookSlug); 238 $this->bookRepo->destroyBySlug($bookSlug);
237 return redirect('/books'); 239 return redirect('/books');
238 } 240 }
241 +
242 + /**
243 + * Show the Restrictions view.
244 + * @param $bookSlug
245 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
246 + */
247 + public function showRestrict($bookSlug)
248 + {
249 + $book = $this->bookRepo->getBySlug($bookSlug);
250 + $this->checkOwnablePermission('restrictions-manage', $book);
251 + $roles = $this->userRepo->getRestrictableRoles();
252 + return view('books/restrictions', [
253 + 'book' => $book,
254 + 'roles' => $roles
255 + ]);
256 + }
257 +
258 + /**
259 + * Set the restrictions for this book.
260 + * @param $bookSlug
261 + * @param $bookSlug
262 + * @param Request $request
263 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
264 + */
265 + public function restrict($bookSlug, Request $request)
266 + {
267 + $book = $this->bookRepo->getBySlug($bookSlug);
268 + $this->checkOwnablePermission('restrictions-manage', $book);
269 + $this->bookRepo->updateRestrictionsFromRequest($request, $book);
270 + session()->flash('success', 'Page Restrictions Updated');
271 + return redirect($book->getUrl());
272 + }
239 } 273 }
......
1 -<?php 1 +<?php namespace BookStack\Http\Controllers;
2 -
3 -namespace BookStack\Http\Controllers;
4 2
5 use Activity; 3 use Activity;
4 +use BookStack\Repos\UserRepo;
6 use Illuminate\Http\Request; 5 use Illuminate\Http\Request;
7 -
8 -use Illuminate\Support\Facades\Auth;
9 use BookStack\Http\Requests; 6 use BookStack\Http\Requests;
10 -use BookStack\Http\Controllers\Controller;
11 use BookStack\Repos\BookRepo; 7 use BookStack\Repos\BookRepo;
12 use BookStack\Repos\ChapterRepo; 8 use BookStack\Repos\ChapterRepo;
13 use Views; 9 use Views;
...@@ -17,20 +13,22 @@ class ChapterController extends Controller ...@@ -17,20 +13,22 @@ class ChapterController extends Controller
17 13
18 protected $bookRepo; 14 protected $bookRepo;
19 protected $chapterRepo; 15 protected $chapterRepo;
16 + protected $userRepo;
20 17
21 /** 18 /**
22 * ChapterController constructor. 19 * ChapterController constructor.
23 - * @param $bookRepo 20 + * @param BookRepo $bookRepo
24 - * @param $chapterRepo 21 + * @param ChapterRepo $chapterRepo
22 + * @param UserRepo $userRepo
25 */ 23 */
26 - public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo) 24 + public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
27 { 25 {
28 $this->bookRepo = $bookRepo; 26 $this->bookRepo = $bookRepo;
29 $this->chapterRepo = $chapterRepo; 27 $this->chapterRepo = $chapterRepo;
28 + $this->userRepo = $userRepo;
30 parent::__construct(); 29 parent::__construct();
31 } 30 }
32 31
33 -
34 /** 32 /**
35 * Show the form for creating a new chapter. 33 * Show the form for creating a new chapter.
36 * @param $bookSlug 34 * @param $bookSlug
...@@ -38,8 +36,8 @@ class ChapterController extends Controller ...@@ -38,8 +36,8 @@ class ChapterController extends Controller
38 */ 36 */
39 public function create($bookSlug) 37 public function create($bookSlug)
40 { 38 {
41 - $this->checkPermission('chapter-create');
42 $book = $this->bookRepo->getBySlug($bookSlug); 39 $book = $this->bookRepo->getBySlug($bookSlug);
40 + $this->checkOwnablePermission('chapter-create', $book);
43 $this->setPageTitle('Create New Chapter'); 41 $this->setPageTitle('Create New Chapter');
44 return view('chapters/create', ['book' => $book, 'current' => $book]); 42 return view('chapters/create', ['book' => $book, 'current' => $book]);
45 } 43 }
...@@ -52,12 +50,13 @@ class ChapterController extends Controller ...@@ -52,12 +50,13 @@ class ChapterController extends Controller
52 */ 50 */
53 public function store($bookSlug, Request $request) 51 public function store($bookSlug, Request $request)
54 { 52 {
55 - $this->checkPermission('chapter-create');
56 $this->validate($request, [ 53 $this->validate($request, [
57 'name' => 'required|string|max:255' 54 'name' => 'required|string|max:255'
58 ]); 55 ]);
59 56
60 $book = $this->bookRepo->getBySlug($bookSlug); 57 $book = $this->bookRepo->getBySlug($bookSlug);
58 + $this->checkOwnablePermission('chapter-create', $book);
59 +
61 $chapter = $this->chapterRepo->newFromInput($request->all()); 60 $chapter = $this->chapterRepo->newFromInput($request->all());
62 $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id); 61 $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
63 $chapter->priority = $this->bookRepo->getNewPriority($book); 62 $chapter->priority = $this->bookRepo->getNewPriority($book);
...@@ -81,7 +80,14 @@ class ChapterController extends Controller ...@@ -81,7 +80,14 @@ class ChapterController extends Controller
81 $sidebarTree = $this->bookRepo->getChildren($book); 80 $sidebarTree = $this->bookRepo->getChildren($book);
82 Views::add($chapter); 81 Views::add($chapter);
83 $this->setPageTitle($chapter->getShortName()); 82 $this->setPageTitle($chapter->getShortName());
84 - return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]); 83 + $pages = $this->chapterRepo->getChildren($chapter);
84 + return view('chapters/show', [
85 + 'book' => $book,
86 + 'chapter' => $chapter,
87 + 'current' => $chapter,
88 + 'sidebarTree' => $sidebarTree,
89 + 'pages' => $pages
90 + ]);
85 } 91 }
86 92
87 /** 93 /**
...@@ -92,9 +98,9 @@ class ChapterController extends Controller ...@@ -92,9 +98,9 @@ class ChapterController extends Controller
92 */ 98 */
93 public function edit($bookSlug, $chapterSlug) 99 public function edit($bookSlug, $chapterSlug)
94 { 100 {
95 - $this->checkPermission('chapter-update');
96 $book = $this->bookRepo->getBySlug($bookSlug); 101 $book = $this->bookRepo->getBySlug($bookSlug);
97 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 102 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
103 + $this->checkOwnablePermission('chapter-update', $chapter);
98 $this->setPageTitle('Edit Chapter' . $chapter->getShortName()); 104 $this->setPageTitle('Edit Chapter' . $chapter->getShortName());
99 return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); 105 return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
100 } 106 }
...@@ -108,9 +114,9 @@ class ChapterController extends Controller ...@@ -108,9 +114,9 @@ class ChapterController extends Controller
108 */ 114 */
109 public function update(Request $request, $bookSlug, $chapterSlug) 115 public function update(Request $request, $bookSlug, $chapterSlug)
110 { 116 {
111 - $this->checkPermission('chapter-update');
112 $book = $this->bookRepo->getBySlug($bookSlug); 117 $book = $this->bookRepo->getBySlug($bookSlug);
113 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 118 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
119 + $this->checkOwnablePermission('chapter-update', $chapter);
114 $chapter->fill($request->all()); 120 $chapter->fill($request->all());
115 $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id); 121 $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
116 $chapter->updated_by = auth()->user()->id; 122 $chapter->updated_by = auth()->user()->id;
...@@ -127,9 +133,9 @@ class ChapterController extends Controller ...@@ -127,9 +133,9 @@ class ChapterController extends Controller
127 */ 133 */
128 public function showDelete($bookSlug, $chapterSlug) 134 public function showDelete($bookSlug, $chapterSlug)
129 { 135 {
130 - $this->checkPermission('chapter-delete');
131 $book = $this->bookRepo->getBySlug($bookSlug); 136 $book = $this->bookRepo->getBySlug($bookSlug);
132 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 137 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
138 + $this->checkOwnablePermission('chapter-delete', $chapter);
133 $this->setPageTitle('Delete Chapter' . $chapter->getShortName()); 139 $this->setPageTitle('Delete Chapter' . $chapter->getShortName());
134 return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); 140 return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
135 } 141 }
...@@ -142,11 +148,46 @@ class ChapterController extends Controller ...@@ -142,11 +148,46 @@ class ChapterController extends Controller
142 */ 148 */
143 public function destroy($bookSlug, $chapterSlug) 149 public function destroy($bookSlug, $chapterSlug)
144 { 150 {
145 - $this->checkPermission('chapter-delete');
146 $book = $this->bookRepo->getBySlug($bookSlug); 151 $book = $this->bookRepo->getBySlug($bookSlug);
147 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 152 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
153 + $this->checkOwnablePermission('chapter-delete', $chapter);
148 Activity::addMessage('chapter_delete', $book->id, $chapter->name); 154 Activity::addMessage('chapter_delete', $book->id, $chapter->name);
149 $this->chapterRepo->destroy($chapter); 155 $this->chapterRepo->destroy($chapter);
150 return redirect($book->getUrl()); 156 return redirect($book->getUrl());
151 } 157 }
158 +
159 + /**
160 + * Show the Restrictions view.
161 + * @param $bookSlug
162 + * @param $chapterSlug
163 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
164 + */
165 + public function showRestrict($bookSlug, $chapterSlug)
166 + {
167 + $book = $this->bookRepo->getBySlug($bookSlug);
168 + $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
169 + $this->checkOwnablePermission('restrictions-manage', $chapter);
170 + $roles = $this->userRepo->getRestrictableRoles();
171 + return view('chapters/restrictions', [
172 + 'chapter' => $chapter,
173 + 'roles' => $roles
174 + ]);
175 + }
176 +
177 + /**
178 + * Set the restrictions for this chapter.
179 + * @param $bookSlug
180 + * @param $chapterSlug
181 + * @param Request $request
182 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
183 + */
184 + public function restrict($bookSlug, $chapterSlug, Request $request)
185 + {
186 + $book = $this->bookRepo->getBySlug($bookSlug);
187 + $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
188 + $this->checkOwnablePermission('restrictions-manage', $chapter);
189 + $this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
190 + session()->flash('success', 'Page Restrictions Updated');
191 + return redirect($chapter->getUrl());
192 + }
152 } 193 }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 +use BookStack\Ownable;
5 use HttpRequestException; 6 use HttpRequestException;
6 use Illuminate\Foundation\Bus\DispatchesJobs; 7 use Illuminate\Foundation\Bus\DispatchesJobs;
7 use Illuminate\Http\Exception\HttpResponseException; 8 use Illuminate\Http\Exception\HttpResponseException;
...@@ -61,21 +62,19 @@ abstract class Controller extends BaseController ...@@ -61,21 +62,19 @@ abstract class Controller extends BaseController
61 } 62 }
62 63
63 /** 64 /**
64 - * On a permission error redirect to home and display 65 + * On a permission error redirect to home and display.
65 * the error as a notification. 66 * the error as a notification.
66 */ 67 */
67 protected function showPermissionError() 68 protected function showPermissionError()
68 { 69 {
69 Session::flash('error', trans('errors.permission')); 70 Session::flash('error', trans('errors.permission'));
70 - throw new HttpResponseException( 71 + $response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
71 - redirect('/') 72 + throw new HttpResponseException($response);
72 - );
73 } 73 }
74 74
75 /** 75 /**
76 * Checks for a permission. 76 * Checks for a permission.
77 - * 77 + * @param string $permissionName
78 - * @param $permissionName
79 * @return bool|\Illuminate\Http\RedirectResponse 78 * @return bool|\Illuminate\Http\RedirectResponse
80 */ 79 */
81 protected function checkPermission($permissionName) 80 protected function checkPermission($permissionName)
...@@ -83,11 +82,22 @@ abstract class Controller extends BaseController ...@@ -83,11 +82,22 @@ abstract class Controller extends BaseController
83 if (!$this->currentUser || !$this->currentUser->can($permissionName)) { 82 if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
84 $this->showPermissionError(); 83 $this->showPermissionError();
85 } 84 }
86 -
87 return true; 85 return true;
88 } 86 }
89 87
90 /** 88 /**
89 + * Check the current user's permissions against an ownable item.
90 + * @param $permission
91 + * @param Ownable $ownable
92 + * @return bool
93 + */
94 + protected function checkOwnablePermission($permission, Ownable $ownable)
95 + {
96 + if (userCan($permission, $ownable)) return true;
97 + return $this->showPermissionError();
98 + }
99 +
100 + /**
91 * Check if a user has a permission or bypass if the callback is true. 101 * Check if a user has a permission or bypass if the callback is true.
92 * @param $permissionName 102 * @param $permissionName
93 * @param $callback 103 * @param $callback
......
...@@ -24,7 +24,6 @@ class HomeController extends Controller ...@@ -24,7 +24,6 @@ class HomeController extends Controller
24 24
25 /** 25 /**
26 * Display the homepage. 26 * Display the homepage.
27 - *
28 * @return Response 27 * @return Response
29 */ 28 */
30 public function index() 29 public function index()
......
...@@ -64,7 +64,7 @@ class ImageController extends Controller ...@@ -64,7 +64,7 @@ class ImageController extends Controller
64 */ 64 */
65 public function uploadByType($type, Request $request) 65 public function uploadByType($type, Request $request)
66 { 66 {
67 - $this->checkPermission('image-create'); 67 + $this->checkPermission('image-create-all');
68 $this->validate($request, [ 68 $this->validate($request, [
69 'file' => 'image|mimes:jpeg,gif,png' 69 'file' => 'image|mimes:jpeg,gif,png'
70 ]); 70 ]);
...@@ -90,7 +90,7 @@ class ImageController extends Controller ...@@ -90,7 +90,7 @@ class ImageController extends Controller
90 */ 90 */
91 public function getThumbnail($id, $width, $height, $crop) 91 public function getThumbnail($id, $width, $height, $crop)
92 { 92 {
93 - $this->checkPermission('image-create'); 93 + $this->checkPermission('image-create-all');
94 $image = $this->imageRepo->getById($id); 94 $image = $this->imageRepo->getById($id);
95 $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false'); 95 $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
96 return response()->json(['url' => $thumbnailUrl]); 96 return response()->json(['url' => $thumbnailUrl]);
...@@ -104,11 +104,11 @@ class ImageController extends Controller ...@@ -104,11 +104,11 @@ class ImageController extends Controller
104 */ 104 */
105 public function update($imageId, Request $request) 105 public function update($imageId, Request $request)
106 { 106 {
107 - $this->checkPermission('image-update');
108 $this->validate($request, [ 107 $this->validate($request, [
109 'name' => 'required|min:2|string' 108 'name' => 'required|min:2|string'
110 ]); 109 ]);
111 $image = $this->imageRepo->getById($imageId); 110 $image = $this->imageRepo->getById($imageId);
111 + $this->checkOwnablePermission('image-update', $image);
112 $image = $this->imageRepo->updateImageDetails($image, $request->all()); 112 $image = $this->imageRepo->updateImageDetails($image, $request->all());
113 return response()->json($image); 113 return response()->json($image);
114 } 114 }
...@@ -123,8 +123,8 @@ class ImageController extends Controller ...@@ -123,8 +123,8 @@ class ImageController extends Controller
123 */ 123 */
124 public function destroy(PageRepo $pageRepo, Request $request, $id) 124 public function destroy(PageRepo $pageRepo, Request $request, $id)
125 { 125 {
126 - $this->checkPermission('image-delete');
127 $image = $this->imageRepo->getById($id); 126 $image = $this->imageRepo->getById($id);
127 + $this->checkOwnablePermission('image-delete', $image);
128 128
129 // Check if this image is used on any pages 129 // Check if this image is used on any pages
130 $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); 130 $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
......
1 -<?php 1 +<?php namespace BookStack\Http\Controllers;
2 -
3 -namespace BookStack\Http\Controllers;
4 2
5 use Activity; 3 use Activity;
4 +use BookStack\Exceptions\NotFoundException;
5 +use BookStack\Repos\UserRepo;
6 use BookStack\Services\ExportService; 6 use BookStack\Services\ExportService;
7 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
8 -
9 -use Illuminate\Support\Facades\Auth;
10 use BookStack\Http\Requests; 8 use BookStack\Http\Requests;
11 use BookStack\Repos\BookRepo; 9 use BookStack\Repos\BookRepo;
12 use BookStack\Repos\ChapterRepo; 10 use BookStack\Repos\ChapterRepo;
...@@ -21,26 +19,28 @@ class PageController extends Controller ...@@ -21,26 +19,28 @@ class PageController extends Controller
21 protected $bookRepo; 19 protected $bookRepo;
22 protected $chapterRepo; 20 protected $chapterRepo;
23 protected $exportService; 21 protected $exportService;
22 + protected $userRepo;
24 23
25 /** 24 /**
26 * PageController constructor. 25 * PageController constructor.
27 - * @param PageRepo $pageRepo 26 + * @param PageRepo $pageRepo
28 - * @param BookRepo $bookRepo 27 + * @param BookRepo $bookRepo
29 - * @param ChapterRepo $chapterRepo 28 + * @param ChapterRepo $chapterRepo
30 * @param ExportService $exportService 29 * @param ExportService $exportService
30 + * @param UserRepo $userRepo
31 */ 31 */
32 - public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService) 32 + public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
33 { 33 {
34 $this->pageRepo = $pageRepo; 34 $this->pageRepo = $pageRepo;
35 $this->bookRepo = $bookRepo; 35 $this->bookRepo = $bookRepo;
36 $this->chapterRepo = $chapterRepo; 36 $this->chapterRepo = $chapterRepo;
37 $this->exportService = $exportService; 37 $this->exportService = $exportService;
38 + $this->userRepo = $userRepo;
38 parent::__construct(); 39 parent::__construct();
39 } 40 }
40 41
41 /** 42 /**
42 * Show the form for creating a new page. 43 * Show the form for creating a new page.
43 - *
44 * @param $bookSlug 44 * @param $bookSlug
45 * @param bool $chapterSlug 45 * @param bool $chapterSlug
46 * @return Response 46 * @return Response
...@@ -48,23 +48,22 @@ class PageController extends Controller ...@@ -48,23 +48,22 @@ class PageController extends Controller
48 */ 48 */
49 public function create($bookSlug, $chapterSlug = false) 49 public function create($bookSlug, $chapterSlug = false)
50 { 50 {
51 - $this->checkPermission('page-create');
52 $book = $this->bookRepo->getBySlug($bookSlug); 51 $book = $this->bookRepo->getBySlug($bookSlug);
53 $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false; 52 $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
53 + $parent = $chapter ? $chapter : $book;
54 + $this->checkOwnablePermission('page-create', $parent);
54 $this->setPageTitle('Create New Page'); 55 $this->setPageTitle('Create New Page');
55 return view('pages/create', ['book' => $book, 'chapter' => $chapter]); 56 return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
56 } 57 }
57 58
58 /** 59 /**
59 * Store a newly created page in storage. 60 * Store a newly created page in storage.
60 - *
61 * @param Request $request 61 * @param Request $request
62 * @param $bookSlug 62 * @param $bookSlug
63 * @return Response 63 * @return Response
64 */ 64 */
65 public function store(Request $request, $bookSlug) 65 public function store(Request $request, $bookSlug)
66 { 66 {
67 - $this->checkPermission('page-create');
68 $this->validate($request, [ 67 $this->validate($request, [
69 'name' => 'required|string|max:255' 68 'name' => 'required|string|max:255'
70 ]); 69 ]);
...@@ -72,6 +71,8 @@ class PageController extends Controller ...@@ -72,6 +71,8 @@ class PageController extends Controller
72 $input = $request->all(); 71 $input = $request->all();
73 $book = $this->bookRepo->getBySlug($bookSlug); 72 $book = $this->bookRepo->getBySlug($bookSlug);
74 $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null; 73 $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
74 + $parent = $chapterId !== null ? $this->chapterRepo->getById($chapterId) : $book;
75 + $this->checkOwnablePermission('page-create', $parent);
75 $input['priority'] = $this->bookRepo->getNewPriority($book); 76 $input['priority'] = $this->bookRepo->getNewPriority($book);
76 77
77 $page = $this->pageRepo->saveNew($input, $book, $chapterId); 78 $page = $this->pageRepo->saveNew($input, $book, $chapterId);
...@@ -84,7 +85,6 @@ class PageController extends Controller ...@@ -84,7 +85,6 @@ class PageController extends Controller
84 * Display the specified page. 85 * Display the specified page.
85 * If the page is not found via the slug the 86 * If the page is not found via the slug the
86 * revisions are searched for a match. 87 * revisions are searched for a match.
87 - *
88 * @param $bookSlug 88 * @param $bookSlug
89 * @param $pageSlug 89 * @param $pageSlug
90 * @return Response 90 * @return Response
...@@ -95,7 +95,7 @@ class PageController extends Controller ...@@ -95,7 +95,7 @@ class PageController extends Controller
95 95
96 try { 96 try {
97 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 97 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
98 - } catch (NotFoundHttpException $e) { 98 + } catch (NotFoundException $e) {
99 $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug); 99 $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
100 if ($page === null) abort(404); 100 if ($page === null) abort(404);
101 return redirect($page->getUrl()); 101 return redirect($page->getUrl());
...@@ -109,23 +109,21 @@ class PageController extends Controller ...@@ -109,23 +109,21 @@ class PageController extends Controller
109 109
110 /** 110 /**
111 * Show the form for editing the specified page. 111 * Show the form for editing the specified page.
112 - *
113 * @param $bookSlug 112 * @param $bookSlug
114 * @param $pageSlug 113 * @param $pageSlug
115 * @return Response 114 * @return Response
116 */ 115 */
117 public function edit($bookSlug, $pageSlug) 116 public function edit($bookSlug, $pageSlug)
118 { 117 {
119 - $this->checkPermission('page-update');
120 $book = $this->bookRepo->getBySlug($bookSlug); 118 $book = $this->bookRepo->getBySlug($bookSlug);
121 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 119 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
120 + $this->checkOwnablePermission('page-update', $page);
122 $this->setPageTitle('Editing Page ' . $page->getShortName()); 121 $this->setPageTitle('Editing Page ' . $page->getShortName());
123 return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); 122 return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
124 } 123 }
125 124
126 /** 125 /**
127 * Update the specified page in storage. 126 * Update the specified page in storage.
128 - *
129 * @param Request $request 127 * @param Request $request
130 * @param $bookSlug 128 * @param $bookSlug
131 * @param $pageSlug 129 * @param $pageSlug
...@@ -133,12 +131,12 @@ class PageController extends Controller ...@@ -133,12 +131,12 @@ class PageController extends Controller
133 */ 131 */
134 public function update(Request $request, $bookSlug, $pageSlug) 132 public function update(Request $request, $bookSlug, $pageSlug)
135 { 133 {
136 - $this->checkPermission('page-update');
137 $this->validate($request, [ 134 $this->validate($request, [
138 'name' => 'required|string|max:255' 135 'name' => 'required|string|max:255'
139 ]); 136 ]);
140 $book = $this->bookRepo->getBySlug($bookSlug); 137 $book = $this->bookRepo->getBySlug($bookSlug);
141 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 138 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
139 + $this->checkOwnablePermission('page-update', $page);
142 $this->pageRepo->updatePage($page, $book->id, $request->all()); 140 $this->pageRepo->updatePage($page, $book->id, $request->all());
143 Activity::add($page, 'page_update', $book->id); 141 Activity::add($page, 'page_update', $book->id);
144 return redirect($page->getUrl()); 142 return redirect($page->getUrl());
...@@ -164,9 +162,9 @@ class PageController extends Controller ...@@ -164,9 +162,9 @@ class PageController extends Controller
164 */ 162 */
165 public function showDelete($bookSlug, $pageSlug) 163 public function showDelete($bookSlug, $pageSlug)
166 { 164 {
167 - $this->checkPermission('page-delete');
168 $book = $this->bookRepo->getBySlug($bookSlug); 165 $book = $this->bookRepo->getBySlug($bookSlug);
169 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 166 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
167 + $this->checkOwnablePermission('page-delete', $page);
170 $this->setPageTitle('Delete Page ' . $page->getShortName()); 168 $this->setPageTitle('Delete Page ' . $page->getShortName());
171 return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); 169 return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
172 } 170 }
...@@ -181,9 +179,9 @@ class PageController extends Controller ...@@ -181,9 +179,9 @@ class PageController extends Controller
181 */ 179 */
182 public function destroy($bookSlug, $pageSlug) 180 public function destroy($bookSlug, $pageSlug)
183 { 181 {
184 - $this->checkPermission('page-delete');
185 $book = $this->bookRepo->getBySlug($bookSlug); 182 $book = $this->bookRepo->getBySlug($bookSlug);
186 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 183 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
184 + $this->checkOwnablePermission('page-delete', $page);
187 Activity::addMessage('page_delete', $book->id, $page->name); 185 Activity::addMessage('page_delete', $book->id, $page->name);
188 $this->pageRepo->destroy($page); 186 $this->pageRepo->destroy($page);
189 return redirect($book->getUrl()); 187 return redirect($book->getUrl());
...@@ -229,9 +227,9 @@ class PageController extends Controller ...@@ -229,9 +227,9 @@ class PageController extends Controller
229 */ 227 */
230 public function restoreRevision($bookSlug, $pageSlug, $revisionId) 228 public function restoreRevision($bookSlug, $pageSlug, $revisionId)
231 { 229 {
232 - $this->checkPermission('page-update');
233 $book = $this->bookRepo->getBySlug($bookSlug); 230 $book = $this->bookRepo->getBySlug($bookSlug);
234 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 231 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
232 + $this->checkOwnablePermission('page-update', $page);
235 $page = $this->pageRepo->restoreRevision($page, $book, $revisionId); 233 $page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
236 Activity::add($page, 'page_restore', $book->id); 234 Activity::add($page, 'page_restore', $book->id);
237 return redirect($page->getUrl()); 235 return redirect($page->getUrl());
...@@ -315,4 +313,39 @@ class PageController extends Controller ...@@ -315,4 +313,39 @@ class PageController extends Controller
315 ]); 313 ]);
316 } 314 }
317 315
316 + /**
317 + * Show the Restrictions view.
318 + * @param $bookSlug
319 + * @param $pageSlug
320 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
321 + */
322 + public function showRestrict($bookSlug, $pageSlug)
323 + {
324 + $book = $this->bookRepo->getBySlug($bookSlug);
325 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
326 + $this->checkOwnablePermission('restrictions-manage', $page);
327 + $roles = $this->userRepo->getRestrictableRoles();
328 + return view('pages/restrictions', [
329 + 'page' => $page,
330 + 'roles' => $roles
331 + ]);
332 + }
333 +
334 + /**
335 + * Set the restrictions for this page.
336 + * @param $bookSlug
337 + * @param $pageSlug
338 + * @param Request $request
339 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
340 + */
341 + public function restrict($bookSlug, $pageSlug, Request $request)
342 + {
343 + $book = $this->bookRepo->getBySlug($bookSlug);
344 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
345 + $this->checkOwnablePermission('restrictions-manage', $page);
346 + $this->pageRepo->updateRestrictionsFromRequest($request, $page);
347 + session()->flash('success', 'Page Restrictions Updated');
348 + return redirect($page->getUrl());
349 + }
350 +
318 } 351 }
......
1 +<?php namespace BookStack\Http\Controllers;
2 +
3 +use BookStack\Exceptions\PermissionsException;
4 +use BookStack\Repos\PermissionsRepo;
5 +use Illuminate\Http\Request;
6 +use BookStack\Http\Requests;
7 +
8 +class PermissionController extends Controller
9 +{
10 +
11 + protected $permissionsRepo;
12 +
13 + /**
14 + * PermissionController constructor.
15 + * @param PermissionsRepo $permissionsRepo
16 + */
17 + public function __construct(PermissionsRepo $permissionsRepo)
18 + {
19 + $this->permissionsRepo = $permissionsRepo;
20 + parent::__construct();
21 + }
22 +
23 + /**
24 + * Show a listing of the roles in the system.
25 + */
26 + public function listRoles()
27 + {
28 + $this->checkPermission('user-roles-manage');
29 + $roles = $this->permissionsRepo->getAllRoles();
30 + return view('settings/roles/index', ['roles' => $roles]);
31 + }
32 +
33 + /**
34 + * Show the form to create a new role
35 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
36 + */
37 + public function createRole()
38 + {
39 + $this->checkPermission('user-roles-manage');
40 + return view('settings/roles/create');
41 + }
42 +
43 + /**
44 + * Store a new role in the system.
45 + * @param Request $request
46 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
47 + */
48 + public function storeRole(Request $request)
49 + {
50 + $this->checkPermission('user-roles-manage');
51 + $this->validate($request, [
52 + 'display_name' => 'required|min:3|max:200',
53 + 'description' => 'max:250'
54 + ]);
55 +
56 + $this->permissionsRepo->saveNewRole($request->all());
57 + session()->flash('success', 'Role successfully created');
58 + return redirect('/settings/roles');
59 + }
60 +
61 + /**
62 + * Show the form for editing a user role.
63 + * @param $id
64 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
65 + */
66 + public function editRole($id)
67 + {
68 + $this->checkPermission('user-roles-manage');
69 + $role = $this->permissionsRepo->getRoleById($id);
70 + return view('settings/roles/edit', ['role' => $role]);
71 + }
72 +
73 + /**
74 + * Updates a user role.
75 + * @param $id
76 + * @param Request $request
77 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
78 + */
79 + public function updateRole($id, Request $request)
80 + {
81 + $this->checkPermission('user-roles-manage');
82 + $this->validate($request, [
83 + 'display_name' => 'required|min:3|max:200',
84 + 'description' => 'max:250'
85 + ]);
86 +
87 + $this->permissionsRepo->updateRole($id, $request->all());
88 + session()->flash('success', 'Role successfully updated');
89 + return redirect('/settings/roles');
90 + }
91 +
92 + /**
93 + * Show the view to delete a role.
94 + * Offers the chance to migrate users.
95 + * @param $id
96 + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
97 + */
98 + public function showDeleteRole($id)
99 + {
100 + $this->checkPermission('user-roles-manage');
101 + $role = $this->permissionsRepo->getRoleById($id);
102 + $roles = $this->permissionsRepo->getAllRolesExcept($role);
103 + $blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
104 + $roles->prepend($blankRole);
105 + return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
106 + }
107 +
108 + /**
109 + * Delete a role from the system,
110 + * Migrate from a previous role if set.
111 + * @param $id
112 + * @param Request $request
113 + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
114 + */
115 + public function deleteRole($id, Request $request)
116 + {
117 + $this->checkPermission('user-roles-manage');
118 +
119 + try {
120 + $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
121 + } catch (PermissionsException $e) {
122 + session()->flash('error', $e->getMessage());
123 + return redirect()->back();
124 + }
125 +
126 + session()->flash('success', 'Role successfully deleted');
127 + return redirect('/settings/roles');
128 + }
129 +}
...@@ -17,7 +17,7 @@ class SettingController extends Controller ...@@ -17,7 +17,7 @@ class SettingController extends Controller
17 */ 17 */
18 public function index() 18 public function index()
19 { 19 {
20 - $this->checkPermission('settings-update'); 20 + $this->checkPermission('settings-manage');
21 $this->setPageTitle('Settings'); 21 $this->setPageTitle('Settings');
22 return view('settings/index'); 22 return view('settings/index');
23 } 23 }
...@@ -32,7 +32,7 @@ class SettingController extends Controller ...@@ -32,7 +32,7 @@ class SettingController extends Controller
32 public function update(Request $request) 32 public function update(Request $request)
33 { 33 {
34 $this->preventAccessForDemoUsers(); 34 $this->preventAccessForDemoUsers();
35 - $this->checkPermission('settings-update'); 35 + $this->checkPermission('settings-manage');
36 36
37 // Cycles through posted settings and update them 37 // Cycles through posted settings and update them
38 foreach($request->all() as $name => $value) { 38 foreach($request->all() as $name => $value) {
......
...@@ -35,7 +35,8 @@ class UserController extends Controller ...@@ -35,7 +35,8 @@ class UserController extends Controller
35 */ 35 */
36 public function index() 36 public function index()
37 { 37 {
38 - $users = $this->user->all(); 38 + $this->checkPermission('users-manage');
39 + $users = $this->userRepo->getAllUsers();
39 $this->setPageTitle('Users'); 40 $this->setPageTitle('Users');
40 return view('users/index', ['users' => $users]); 41 return view('users/index', ['users' => $users]);
41 } 42 }
...@@ -46,7 +47,7 @@ class UserController extends Controller ...@@ -46,7 +47,7 @@ class UserController extends Controller
46 */ 47 */
47 public function create() 48 public function create()
48 { 49 {
49 - $this->checkPermission('user-create'); 50 + $this->checkPermission('users-manage');
50 $authMethod = config('auth.method'); 51 $authMethod = config('auth.method');
51 return view('users/create', ['authMethod' => $authMethod]); 52 return view('users/create', ['authMethod' => $authMethod]);
52 } 53 }
...@@ -58,11 +59,10 @@ class UserController extends Controller ...@@ -58,11 +59,10 @@ class UserController extends Controller
58 */ 59 */
59 public function store(Request $request) 60 public function store(Request $request)
60 { 61 {
61 - $this->checkPermission('user-create'); 62 + $this->checkPermission('users-manage');
62 $validationRules = [ 63 $validationRules = [
63 'name' => 'required', 64 'name' => 'required',
64 - 'email' => 'required|email|unique:users,email', 65 + 'email' => 'required|email|unique:users,email'
65 - 'role' => 'required|exists:roles,id'
66 ]; 66 ];
67 67
68 $authMethod = config('auth.method'); 68 $authMethod = config('auth.method');
...@@ -84,7 +84,11 @@ class UserController extends Controller ...@@ -84,7 +84,11 @@ class UserController extends Controller
84 } 84 }
85 85
86 $user->save(); 86 $user->save();
87 - $user->attachRoleId($request->get('role')); 87 +
88 + if ($request->has('roles')) {
89 + $roles = $request->get('roles');
90 + $user->roles()->sync($roles);
91 + }
88 92
89 // Get avatar from gravatar and save 93 // Get avatar from gravatar and save
90 if (!config('services.disable_services')) { 94 if (!config('services.disable_services')) {
...@@ -104,7 +108,7 @@ class UserController extends Controller ...@@ -104,7 +108,7 @@ class UserController extends Controller
104 */ 108 */
105 public function edit($id, SocialAuthService $socialAuthService) 109 public function edit($id, SocialAuthService $socialAuthService)
106 { 110 {
107 - $this->checkPermissionOr('user-update', function () use ($id) { 111 + $this->checkPermissionOr('users-manage', function () use ($id) {
108 return $this->currentUser->id == $id; 112 return $this->currentUser->id == $id;
109 }); 113 });
110 114
...@@ -125,7 +129,7 @@ class UserController extends Controller ...@@ -125,7 +129,7 @@ class UserController extends Controller
125 public function update(Request $request, $id) 129 public function update(Request $request, $id)
126 { 130 {
127 $this->preventAccessForDemoUsers(); 131 $this->preventAccessForDemoUsers();
128 - $this->checkPermissionOr('user-update', function () use ($id) { 132 + $this->checkPermissionOr('users-manage', function () use ($id) {
129 return $this->currentUser->id == $id; 133 return $this->currentUser->id == $id;
130 }); 134 });
131 135
...@@ -133,8 +137,7 @@ class UserController extends Controller ...@@ -133,8 +137,7 @@ class UserController extends Controller
133 'name' => 'min:2', 137 'name' => 'min:2',
134 'email' => 'min:2|email|unique:users,email,' . $id, 138 'email' => 'min:2|email|unique:users,email,' . $id,
135 'password' => 'min:5|required_with:password_confirm', 139 'password' => 'min:5|required_with:password_confirm',
136 - 'password-confirm' => 'same:password|required_with:password', 140 + 'password-confirm' => 'same:password|required_with:password'
137 - 'role' => 'exists:roles,id'
138 ], [ 141 ], [
139 'password-confirm.required_with' => 'Password confirmation required' 142 'password-confirm.required_with' => 'Password confirmation required'
140 ]); 143 ]);
...@@ -143,8 +146,9 @@ class UserController extends Controller ...@@ -143,8 +146,9 @@ class UserController extends Controller
143 $user->fill($request->all()); 146 $user->fill($request->all());
144 147
145 // Role updates 148 // Role updates
146 - if ($this->currentUser->can('user-update') && $request->has('role')) { 149 + if (userCan('users-manage') && $request->has('roles')) {
147 - $user->attachRoleId($request->get('role')); 150 + $roles = $request->get('roles');
151 + $user->roles()->sync($roles);
148 } 152 }
149 153
150 // Password updates 154 // Password updates
...@@ -154,11 +158,12 @@ class UserController extends Controller ...@@ -154,11 +158,12 @@ class UserController extends Controller
154 } 158 }
155 159
156 // External auth id updates 160 // External auth id updates
157 - if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) { 161 + if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
158 $user->external_auth_id = $request->get('external_auth_id'); 162 $user->external_auth_id = $request->get('external_auth_id');
159 } 163 }
160 164
161 $user->save(); 165 $user->save();
166 + session()->flash('success', 'User successfully updated');
162 return redirect('/settings/users'); 167 return redirect('/settings/users');
163 } 168 }
164 169
...@@ -169,7 +174,7 @@ class UserController extends Controller ...@@ -169,7 +174,7 @@ class UserController extends Controller
169 */ 174 */
170 public function delete($id) 175 public function delete($id)
171 { 176 {
172 - $this->checkPermissionOr('user-delete', function () use ($id) { 177 + $this->checkPermissionOr('users-manage', function () use ($id) {
173 return $this->currentUser->id == $id; 178 return $this->currentUser->id == $id;
174 }); 179 });
175 180
...@@ -186,7 +191,7 @@ class UserController extends Controller ...@@ -186,7 +191,7 @@ class UserController extends Controller
186 public function destroy($id) 191 public function destroy($id)
187 { 192 {
188 $this->preventAccessForDemoUsers(); 193 $this->preventAccessForDemoUsers();
189 - $this->checkPermissionOr('user-delete', function () use ($id) { 194 + $this->checkPermissionOr('users-manage', function () use ($id) {
190 return $this->currentUser->id == $id; 195 return $this->currentUser->id == $id;
191 }); 196 });
192 197
......
...@@ -19,6 +19,8 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -19,6 +19,8 @@ Route::group(['middleware' => 'auth'], function () {
19 Route::delete('/{id}', 'BookController@destroy'); 19 Route::delete('/{id}', 'BookController@destroy');
20 Route::get('/{slug}/sort-item', 'BookController@getSortItem'); 20 Route::get('/{slug}/sort-item', 'BookController@getSortItem');
21 Route::get('/{slug}', 'BookController@show'); 21 Route::get('/{slug}', 'BookController@show');
22 + Route::get('/{bookSlug}/restrict', 'BookController@showRestrict');
23 + Route::put('/{bookSlug}/restrict', 'BookController@restrict');
22 Route::get('/{slug}/delete', 'BookController@showDelete'); 24 Route::get('/{slug}/delete', 'BookController@showDelete');
23 Route::get('/{bookSlug}/sort', 'BookController@sort'); 25 Route::get('/{bookSlug}/sort', 'BookController@sort');
24 Route::put('/{bookSlug}/sort', 'BookController@saveSort'); 26 Route::put('/{bookSlug}/sort', 'BookController@saveSort');
...@@ -32,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -32,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () {
32 Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); 34 Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
33 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); 35 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
34 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); 36 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
37 + Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict');
38 + Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
35 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); 39 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
36 Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy'); 40 Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
37 41
...@@ -47,6 +51,8 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -47,6 +51,8 @@ Route::group(['middleware' => 'auth'], function () {
47 Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); 51 Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
48 Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update'); 52 Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
49 Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit'); 53 Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
54 + Route::get('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@showRestrict');
55 + Route::put('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@restrict');
50 Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete'); 56 Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
51 Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy'); 57 Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
52 58
...@@ -87,6 +93,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -87,6 +93,7 @@ Route::group(['middleware' => 'auth'], function () {
87 Route::group(['prefix' => 'settings'], function() { 93 Route::group(['prefix' => 'settings'], function() {
88 Route::get('/', 'SettingController@index'); 94 Route::get('/', 'SettingController@index');
89 Route::post('/', 'SettingController@update'); 95 Route::post('/', 'SettingController@update');
96 +
90 // Users 97 // Users
91 Route::get('/users', 'UserController@index'); 98 Route::get('/users', 'UserController@index');
92 Route::get('/users/create', 'UserController@create'); 99 Route::get('/users/create', 'UserController@create');
...@@ -95,6 +102,15 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -95,6 +102,15 @@ Route::group(['middleware' => 'auth'], function () {
95 Route::get('/users/{id}', 'UserController@edit'); 102 Route::get('/users/{id}', 'UserController@edit');
96 Route::put('/users/{id}', 'UserController@update'); 103 Route::put('/users/{id}', 'UserController@update');
97 Route::delete('/users/{id}', 'UserController@destroy'); 104 Route::delete('/users/{id}', 'UserController@destroy');
105 +
106 + // Roles
107 + Route::get('/roles', 'PermissionController@listRoles');
108 + Route::get('/roles/new', 'PermissionController@createRole');
109 + Route::post('/roles/new', 'PermissionController@storeRole');
110 + Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
111 + Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
112 + Route::get('/roles/{id}', 'PermissionController@editRole');
113 + Route::put('/roles/{id}', 'PermissionController@updateRole');
98 }); 114 });
99 115
100 }); 116 });
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 -
5 -
6 -use Illuminate\Database\Eloquent\Model;
7 use Images; 3 use Images;
8 4
9 -class Image extends Model 5 +class Image extends Ownable
10 { 6 {
11 - use Ownable;
12 7
13 protected $fillable = ['name']; 8 protected $fillable = ['name'];
14 9
......
1 <?php namespace BookStack; 1 <?php namespace BookStack;
2 2
3 +use Illuminate\Database\Eloquent\Model;
3 4
4 -trait Ownable 5 +abstract class Ownable extends Model
5 { 6 {
6 /** 7 /**
7 * Relation for the user that created this entity. 8 * Relation for the user that created this entity.
...@@ -20,4 +21,14 @@ trait Ownable ...@@ -20,4 +21,14 @@ trait Ownable
20 { 21 {
21 return $this->belongsTo('BookStack\User', 'updated_by'); 22 return $this->belongsTo('BookStack\User', 'updated_by');
22 } 23 }
24 +
25 + /**
26 + * Gets the class name.
27 + * @return string
28 + */
29 + public static function getClassName()
30 + {
31 + return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
32 + }
33 +
23 } 34 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -13,4 +13,14 @@ class Permission extends Model ...@@ -13,4 +13,14 @@ class Permission extends Model
13 { 13 {
14 return $this->belongsToMany('BookStack\Permissions'); 14 return $this->belongsToMany('BookStack\Permissions');
15 } 15 }
16 +
17 + /**
18 + * Get the permission object by name.
19 + * @param $roleName
20 + * @return mixed
21 + */
22 + public static function getByName($name)
23 + {
24 + return static::where('name', '=', $name)->first();
25 + }
16 } 26 }
......
...@@ -28,11 +28,17 @@ class CustomFacadeProvider extends ServiceProvider ...@@ -28,11 +28,17 @@ class CustomFacadeProvider extends ServiceProvider
28 public function register() 28 public function register()
29 { 29 {
30 $this->app->bind('activity', function() { 30 $this->app->bind('activity', function() {
31 - return new ActivityService($this->app->make('BookStack\Activity')); 31 + return new ActivityService(
32 + $this->app->make('BookStack\Activity'),
33 + $this->app->make('BookStack\Services\RestrictionService')
34 + );
32 }); 35 });
33 36
34 $this->app->bind('views', function() { 37 $this->app->bind('views', function() {
35 - return new ViewService($this->app->make('BookStack\View')); 38 + return new ViewService(
39 + $this->app->make('BookStack\View'),
40 + $this->app->make('BookStack\Services\RestrictionService')
41 + );
36 }); 42 });
37 43
38 $this->app->bind('setting', function() { 44 $this->app->bind('setting', function() {
...@@ -41,6 +47,7 @@ class CustomFacadeProvider extends ServiceProvider ...@@ -41,6 +47,7 @@ class CustomFacadeProvider extends ServiceProvider
41 $this->app->make('Illuminate\Contracts\Cache\Repository') 47 $this->app->make('Illuminate\Contracts\Cache\Repository')
42 ); 48 );
43 }); 49 });
50 +
44 $this->app->bind('images', function() { 51 $this->app->bind('images', function() {
45 return new ImageService( 52 return new ImageService(
46 $this->app->make('Intervention\Image\ImageManager'), 53 $this->app->make('Intervention\Image\ImageManager'),
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 -use Activity; 3 +use BookStack\Exceptions\NotFoundException;
4 use Illuminate\Support\Str; 4 use Illuminate\Support\Str;
5 use BookStack\Book; 5 use BookStack\Book;
6 use Views; 6 use Views;
7 7
8 -class BookRepo 8 +class BookRepo extends EntityRepo
9 { 9 {
10 -
11 - protected $book;
12 protected $pageRepo; 10 protected $pageRepo;
13 protected $chapterRepo; 11 protected $chapterRepo;
14 12
15 /** 13 /**
16 * BookRepo constructor. 14 * BookRepo constructor.
17 - * @param Book $book
18 * @param PageRepo $pageRepo 15 * @param PageRepo $pageRepo
19 * @param ChapterRepo $chapterRepo 16 * @param ChapterRepo $chapterRepo
20 */ 17 */
21 - public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo) 18 + public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
22 { 19 {
23 - $this->book = $book;
24 $this->pageRepo = $pageRepo; 20 $this->pageRepo = $pageRepo;
25 $this->chapterRepo = $chapterRepo; 21 $this->chapterRepo = $chapterRepo;
22 + parent::__construct();
23 + }
24 +
25 + /**
26 + * Base query for getting books.
27 + * Takes into account any restrictions.
28 + * @return mixed
29 + */
30 + private function bookQuery()
31 + {
32 + return $this->restrictionService->enforceBookRestrictions($this->book, 'view');
26 } 33 }
27 34
28 /** 35 /**
...@@ -32,7 +39,7 @@ class BookRepo ...@@ -32,7 +39,7 @@ class BookRepo
32 */ 39 */
33 public function getById($id) 40 public function getById($id)
34 { 41 {
35 - return $this->book->findOrFail($id); 42 + return $this->bookQuery()->findOrFail($id);
36 } 43 }
37 44
38 /** 45 /**
...@@ -42,7 +49,7 @@ class BookRepo ...@@ -42,7 +49,7 @@ class BookRepo
42 */ 49 */
43 public function getAll($count = 10) 50 public function getAll($count = 10)
44 { 51 {
45 - $bookQuery = $this->book->orderBy('name', 'asc'); 52 + $bookQuery = $this->bookQuery()->orderBy('name', 'asc');
46 if (!$count) return $bookQuery->get(); 53 if (!$count) return $bookQuery->get();
47 return $bookQuery->take($count)->get(); 54 return $bookQuery->take($count)->get();
48 } 55 }
...@@ -54,7 +61,8 @@ class BookRepo ...@@ -54,7 +61,8 @@ class BookRepo
54 */ 61 */
55 public function getAllPaginated($count = 10) 62 public function getAllPaginated($count = 10)
56 { 63 {
57 - return $this->book->orderBy('name', 'asc')->paginate($count); 64 + return $this->bookQuery()
65 + ->orderBy('name', 'asc')->paginate($count);
58 } 66 }
59 67
60 68
...@@ -65,7 +73,7 @@ class BookRepo ...@@ -65,7 +73,7 @@ class BookRepo
65 */ 73 */
66 public function getLatest($count = 10) 74 public function getLatest($count = 10)
67 { 75 {
68 - return $this->book->orderBy('created_at', 'desc')->take($count)->get(); 76 + return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
69 } 77 }
70 78
71 /** 79 /**
...@@ -94,11 +102,12 @@ class BookRepo ...@@ -94,11 +102,12 @@ class BookRepo
94 * Get a book by slug 102 * Get a book by slug
95 * @param $slug 103 * @param $slug
96 * @return mixed 104 * @return mixed
105 + * @throws NotFoundException
97 */ 106 */
98 public function getBySlug($slug) 107 public function getBySlug($slug)
99 { 108 {
100 - $book = $this->book->where('slug', '=', $slug)->first(); 109 + $book = $this->bookQuery()->where('slug', '=', $slug)->first();
101 - if ($book === null) abort(404); 110 + if ($book === null) throw new NotFoundException('Book not found');
102 return $book; 111 return $book;
103 } 112 }
104 113
...@@ -109,7 +118,7 @@ class BookRepo ...@@ -109,7 +118,7 @@ class BookRepo
109 */ 118 */
110 public function exists($id) 119 public function exists($id)
111 { 120 {
112 - return $this->book->where('id', '=', $id)->exists(); 121 + return $this->bookQuery()->where('id', '=', $id)->exists();
113 } 122 }
114 123
115 /** 124 /**
...@@ -119,17 +128,7 @@ class BookRepo ...@@ -119,17 +128,7 @@ class BookRepo
119 */ 128 */
120 public function newFromInput($input) 129 public function newFromInput($input)
121 { 130 {
122 - return $this->book->fill($input); 131 + return $this->book->newInstance($input);
123 - }
124 -
125 - /**
126 - * Count the amount of books that have a specific slug.
127 - * @param $slug
128 - * @return mixed
129 - */
130 - public function countBySlug($slug)
131 - {
132 - return $this->book->where('slug', '=', $slug)->count();
133 } 132 }
134 133
135 /** 134 /**
...@@ -146,6 +145,7 @@ class BookRepo ...@@ -146,6 +145,7 @@ class BookRepo
146 $this->chapterRepo->destroy($chapter); 145 $this->chapterRepo->destroy($chapter);
147 } 146 }
148 $book->views()->delete(); 147 $book->views()->delete();
148 + $book->restrictions()->delete();
149 $book->delete(); 149 $book->delete();
150 } 150 }
151 151
...@@ -202,8 +202,15 @@ class BookRepo ...@@ -202,8 +202,15 @@ class BookRepo
202 */ 202 */
203 public function getChildren(Book $book) 203 public function getChildren(Book $book)
204 { 204 {
205 - $pages = $book->pages()->where('chapter_id', '=', 0)->get(); 205 + $pageQuery = $book->pages()->where('chapter_id', '=', 0);
206 - $chapters = $book->chapters()->with('pages')->get(); 206 + $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
207 + $pages = $pageQuery->get();
208 +
209 + $chapterQuery = $book->chapters()->with(['pages' => function($query) {
210 + $this->restrictionService->enforcePageRestrictions($query, 'view');
211 + }]);
212 + $chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
213 + $chapters = $chapterQuery->get();
207 $children = $pages->merge($chapters); 214 $children = $pages->merge($chapters);
208 $bookSlug = $book->slug; 215 $bookSlug = $book->slug;
209 $children->each(function ($child) use ($bookSlug) { 216 $children->each(function ($child) use ($bookSlug) {
...@@ -226,8 +233,8 @@ class BookRepo ...@@ -226,8 +233,8 @@ class BookRepo
226 */ 233 */
227 public function getBySearch($term, $count = 20, $paginationAppends = []) 234 public function getBySearch($term, $count = 20, $paginationAppends = [])
228 { 235 {
229 - $terms = explode(' ', $term); 236 + $terms = $this->prepareSearchTerms($term);
230 - $books = $this->book->fullTextSearchQuery(['name', 'description'], $terms) 237 + $books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
231 ->paginate($count)->appends($paginationAppends); 238 ->paginate($count)->appends($paginationAppends);
232 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 239 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
233 foreach ($books as $book) { 240 foreach ($books as $book) {
......
...@@ -2,21 +2,19 @@ ...@@ -2,21 +2,19 @@
2 2
3 3
4 use Activity; 4 use Activity;
5 +use BookStack\Exceptions\NotFoundException;
5 use Illuminate\Support\Str; 6 use Illuminate\Support\Str;
6 use BookStack\Chapter; 7 use BookStack\Chapter;
7 8
8 -class ChapterRepo 9 +class ChapterRepo extends EntityRepo
9 { 10 {
10 -
11 - protected $chapter;
12 -
13 /** 11 /**
14 - * ChapterRepo constructor. 12 + * Base query for getting chapters, Takes restrictions into account.
15 - * @param $chapter 13 + * @return mixed
16 */ 14 */
17 - public function __construct(Chapter $chapter) 15 + private function chapterQuery()
18 { 16 {
19 - $this->chapter = $chapter; 17 + return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
20 } 18 }
21 19
22 /** 20 /**
...@@ -26,7 +24,7 @@ class ChapterRepo ...@@ -26,7 +24,7 @@ class ChapterRepo
26 */ 24 */
27 public function idExists($id) 25 public function idExists($id)
28 { 26 {
29 - return $this->chapter->where('id', '=', $id)->count() > 0; 27 + return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
30 } 28 }
31 29
32 /** 30 /**
...@@ -36,7 +34,7 @@ class ChapterRepo ...@@ -36,7 +34,7 @@ class ChapterRepo
36 */ 34 */
37 public function getById($id) 35 public function getById($id)
38 { 36 {
39 - return $this->chapter->findOrFail($id); 37 + return $this->chapterQuery()->findOrFail($id);
40 } 38 }
41 39
42 /** 40 /**
...@@ -45,7 +43,7 @@ class ChapterRepo ...@@ -45,7 +43,7 @@ class ChapterRepo
45 */ 43 */
46 public function getAll() 44 public function getAll()
47 { 45 {
48 - return $this->chapter->all(); 46 + return $this->chapterQuery()->all();
49 } 47 }
50 48
51 /** 49 /**
...@@ -53,15 +51,25 @@ class ChapterRepo ...@@ -53,15 +51,25 @@ class ChapterRepo
53 * @param $slug 51 * @param $slug
54 * @param $bookId 52 * @param $bookId
55 * @return mixed 53 * @return mixed
54 + * @throws NotFoundException
56 */ 55 */
57 public function getBySlug($slug, $bookId) 56 public function getBySlug($slug, $bookId)
58 { 57 {
59 - $chapter = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); 58 + $chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
60 - if ($chapter === null) abort(404); 59 + if ($chapter === null) throw new NotFoundException('Chapter not found');
61 return $chapter; 60 return $chapter;
62 } 61 }
63 62
64 /** 63 /**
64 + * Get the child items for a chapter
65 + * @param Chapter $chapter
66 + */
67 + public function getChildren(Chapter $chapter)
68 + {
69 + return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
70 + }
71 +
72 + /**
65 * Create a new chapter from request input. 73 * Create a new chapter from request input.
66 * @param $input 74 * @param $input
67 * @return $this 75 * @return $this
...@@ -85,6 +93,7 @@ class ChapterRepo ...@@ -85,6 +93,7 @@ class ChapterRepo
85 } 93 }
86 Activity::removeEntity($chapter); 94 Activity::removeEntity($chapter);
87 $chapter->views()->delete(); 95 $chapter->views()->delete();
96 + $chapter->restrictions()->delete();
88 $chapter->delete(); 97 $chapter->delete();
89 } 98 }
90 99
...@@ -123,7 +132,7 @@ class ChapterRepo ...@@ -123,7 +132,7 @@ class ChapterRepo
123 132
124 /** 133 /**
125 * Get chapters by the given search term. 134 * Get chapters by the given search term.
126 - * @param $term 135 + * @param string $term
127 * @param array $whereTerms 136 * @param array $whereTerms
128 * @param int $count 137 * @param int $count
129 * @param array $paginationAppends 138 * @param array $paginationAppends
...@@ -131,8 +140,8 @@ class ChapterRepo ...@@ -131,8 +140,8 @@ class ChapterRepo
131 */ 140 */
132 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) 141 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
133 { 142 {
134 - $terms = explode(' ', $term); 143 + $terms = $this->prepareSearchTerms($term);
135 - $chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms) 144 + $chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
136 ->paginate($count)->appends($paginationAppends); 145 ->paginate($count)->appends($paginationAppends);
137 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 146 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
138 foreach ($chapters as $chapter) { 147 foreach ($chapters as $chapter) {
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 -
4 use BookStack\Book; 3 use BookStack\Book;
5 use BookStack\Chapter; 4 use BookStack\Chapter;
5 +use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 +use BookStack\Services\RestrictionService;
7 8
8 class EntityRepo 9 class EntityRepo
9 { 10 {
10 11
12 + /**
13 + * @var Book $book
14 + */
11 public $book; 15 public $book;
16 +
17 + /**
18 + * @var Chapter
19 + */
12 public $chapter; 20 public $chapter;
21 +
22 + /**
23 + * @var Page
24 + */
13 public $page; 25 public $page;
14 26
15 /** 27 /**
28 + * @var RestrictionService
29 + */
30 + protected $restrictionService;
31 +
32 + /**
16 * EntityService constructor. 33 * EntityService constructor.
17 - * @param $book
18 - * @param $chapter
19 - * @param $page
20 */ 34 */
21 - public function __construct(Book $book, Chapter $chapter, Page $page) 35 + public function __construct()
22 { 36 {
23 - $this->book = $book; 37 + $this->book = app(Book::class);
24 - $this->chapter = $chapter; 38 + $this->chapter = app(Chapter::class);
25 - $this->page = $page; 39 + $this->page = app(Page::class);
40 + $this->restrictionService = app(RestrictionService::class);
26 } 41 }
27 42
28 /** 43 /**
...@@ -32,7 +47,8 @@ class EntityRepo ...@@ -32,7 +47,8 @@ class EntityRepo
32 */ 47 */
33 public function getRecentlyCreatedBooks($count = 20, $page = 0) 48 public function getRecentlyCreatedBooks($count = 20, $page = 0)
34 { 49 {
35 - return $this->book->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get(); 50 + return $this->restrictionService->enforceBookRestrictions($this->book)
51 + ->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get();
36 } 52 }
37 53
38 /** 54 /**
...@@ -43,7 +59,8 @@ class EntityRepo ...@@ -43,7 +59,8 @@ class EntityRepo
43 */ 59 */
44 public function getRecentlyUpdatedBooks($count = 20, $page = 0) 60 public function getRecentlyUpdatedBooks($count = 20, $page = 0)
45 { 61 {
46 - return $this->book->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get(); 62 + return $this->restrictionService->enforceBookRestrictions($this->book)
63 + ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
47 } 64 }
48 65
49 /** 66 /**
...@@ -53,7 +70,8 @@ class EntityRepo ...@@ -53,7 +70,8 @@ class EntityRepo
53 */ 70 */
54 public function getRecentlyCreatedPages($count = 20, $page = 0) 71 public function getRecentlyCreatedPages($count = 20, $page = 0)
55 { 72 {
56 - return $this->page->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get(); 73 + return $this->restrictionService->enforcePageRestrictions($this->page)
74 + ->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get();
57 } 75 }
58 76
59 /** 77 /**
...@@ -64,7 +82,50 @@ class EntityRepo ...@@ -64,7 +82,50 @@ class EntityRepo
64 */ 82 */
65 public function getRecentlyUpdatedPages($count = 20, $page = 0) 83 public function getRecentlyUpdatedPages($count = 20, $page = 0)
66 { 84 {
67 - return $this->page->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get(); 85 + return $this->restrictionService->enforcePageRestrictions($this->page)
86 + ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
87 + }
88 +
89 + /**
90 + * Updates entity restrictions from a request
91 + * @param $request
92 + * @param Entity $entity
93 + */
94 + public function updateRestrictionsFromRequest($request, Entity $entity)
95 + {
96 + $entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
97 + $entity->restrictions()->delete();
98 + if ($request->has('restrictions')) {
99 + foreach ($request->get('restrictions') as $roleId => $restrictions) {
100 + foreach ($restrictions as $action => $value) {
101 + $entity->restrictions()->create([
102 + 'role_id' => $roleId,
103 + 'action' => strtolower($action)
104 + ]);
105 + }
106 + }
107 + }
108 + $entity->save();
109 + }
110 +
111 + /**
112 + * Prepare a string of search terms by turning
113 + * it into an array of terms.
114 + * Keeps quoted terms together.
115 + * @param $termString
116 + * @return array
117 + */
118 + protected function prepareSearchTerms($termString)
119 + {
120 + preg_match_all('/"(.*?)"/', $termString, $matches);
121 + if (count($matches[1]) > 0) {
122 + $terms = $matches[1];
123 + $termString = trim(preg_replace('/"(.*?)"/', '', $termString));
124 + } else {
125 + $terms = [];
126 + }
127 + if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
128 + return $terms;
68 } 129 }
69 130
70 131
......
...@@ -3,39 +3,32 @@ ...@@ -3,39 +3,32 @@
3 3
4 use Activity; 4 use Activity;
5 use BookStack\Book; 5 use BookStack\Book;
6 -use BookStack\Chapter; 6 +use BookStack\Exceptions\NotFoundException;
7 -use Illuminate\Http\Request;
8 -use Illuminate\Support\Facades\Auth;
9 -use Illuminate\Support\Facades\Log;
10 use Illuminate\Support\Str; 7 use Illuminate\Support\Str;
11 use BookStack\Page; 8 use BookStack\Page;
12 use BookStack\PageRevision; 9 use BookStack\PageRevision;
13 -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
14 10
15 -class PageRepo 11 +class PageRepo extends EntityRepo
16 { 12 {
17 - protected $page;
18 protected $pageRevision; 13 protected $pageRevision;
19 14
20 /** 15 /**
21 * PageRepo constructor. 16 * PageRepo constructor.
22 - * @param Page $page
23 * @param PageRevision $pageRevision 17 * @param PageRevision $pageRevision
24 */ 18 */
25 - public function __construct(Page $page, PageRevision $pageRevision) 19 + public function __construct(PageRevision $pageRevision)
26 { 20 {
27 - $this->page = $page;
28 $this->pageRevision = $pageRevision; 21 $this->pageRevision = $pageRevision;
22 + parent::__construct();
29 } 23 }
30 24
31 /** 25 /**
32 - * Check if a page id exists. 26 + * Base query for getting pages, Takes restrictions into account.
33 - * @param $id 27 + * @return mixed
34 - * @return bool
35 */ 28 */
36 - public function idExists($id) 29 + private function pageQuery()
37 { 30 {
38 - return $this->page->where('page_id', '=', $id)->count() > 0; 31 + return $this->restrictionService->enforcePageRestrictions($this->page, 'view');
39 } 32 }
40 33
41 /** 34 /**
...@@ -45,16 +38,7 @@ class PageRepo ...@@ -45,16 +38,7 @@ class PageRepo
45 */ 38 */
46 public function getById($id) 39 public function getById($id)
47 { 40 {
48 - return $this->page->findOrFail($id); 41 + return $this->pageQuery()->findOrFail($id);
49 - }
50 -
51 - /**
52 - * Get all pages.
53 - * @return \Illuminate\Database\Eloquent\Collection|static[]
54 - */
55 - public function getAll()
56 - {
57 - return $this->page->all();
58 } 42 }
59 43
60 /** 44 /**
...@@ -62,11 +46,12 @@ class PageRepo ...@@ -62,11 +46,12 @@ class PageRepo
62 * @param $slug 46 * @param $slug
63 * @param $bookId 47 * @param $bookId
64 * @return mixed 48 * @return mixed
49 + * @throws NotFoundException
65 */ 50 */
66 public function getBySlug($slug, $bookId) 51 public function getBySlug($slug, $bookId)
67 { 52 {
68 - $page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); 53 + $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
69 - if ($page === null) throw new NotFoundHttpException('Page not found'); 54 + if ($page === null) throw new NotFoundException('Page not found');
70 return $page; 55 return $page;
71 } 56 }
72 57
...@@ -81,6 +66,9 @@ class PageRepo ...@@ -81,6 +66,9 @@ class PageRepo
81 public function findPageUsingOldSlug($pageSlug, $bookSlug) 66 public function findPageUsingOldSlug($pageSlug, $bookSlug)
82 { 67 {
83 $revision = $this->pageRevision->where('slug', '=', $pageSlug) 68 $revision = $this->pageRevision->where('slug', '=', $pageSlug)
69 + ->whereHas('page', function($query) {
70 + $this->restrictionService->enforcePageRestrictions($query);
71 + })
84 ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') 72 ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
85 ->with('page')->first(); 73 ->with('page')->first();
86 return $revision !== null ? $revision->page : null; 74 return $revision !== null ? $revision->page : null;
...@@ -201,8 +189,8 @@ class PageRepo ...@@ -201,8 +189,8 @@ class PageRepo
201 */ 189 */
202 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) 190 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
203 { 191 {
204 - $terms = explode(' ', $term); 192 + $terms = $this->prepareSearchTerms($term);
205 - $pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms) 193 + $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
206 ->paginate($count)->appends($paginationAppends); 194 ->paginate($count)->appends($paginationAppends);
207 195
208 // Add highlights to page text. 196 // Add highlights to page text.
...@@ -240,7 +228,7 @@ class PageRepo ...@@ -240,7 +228,7 @@ class PageRepo
240 */ 228 */
241 public function searchForImage($imageString) 229 public function searchForImage($imageString)
242 { 230 {
243 - $pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get(); 231 + $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
244 foreach ($pages as $page) { 232 foreach ($pages as $page) {
245 $page->url = $page->getUrl(); 233 $page->url = $page->getUrl();
246 $page->html = ''; 234 $page->html = '';
...@@ -386,6 +374,7 @@ class PageRepo ...@@ -386,6 +374,7 @@ class PageRepo
386 Activity::removeEntity($page); 374 Activity::removeEntity($page);
387 $page->views()->delete(); 375 $page->views()->delete();
388 $page->revisions()->delete(); 376 $page->revisions()->delete();
377 + $page->restrictions()->delete();
389 $page->delete(); 378 $page->delete();
390 } 379 }
391 380
...@@ -395,7 +384,7 @@ class PageRepo ...@@ -395,7 +384,7 @@ class PageRepo
395 */ 384 */
396 public function getRecentlyCreatedPaginated($count = 20) 385 public function getRecentlyCreatedPaginated($count = 20)
397 { 386 {
398 - return $this->page->orderBy('created_at', 'desc')->paginate($count); 387 + return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
399 } 388 }
400 389
401 /** 390 /**
...@@ -404,7 +393,7 @@ class PageRepo ...@@ -404,7 +393,7 @@ class PageRepo
404 */ 393 */
405 public function getRecentlyUpdatedPaginated($count = 20) 394 public function getRecentlyUpdatedPaginated($count = 20)
406 { 395 {
407 - return $this->page->orderBy('updated_at', 'desc')->paginate($count); 396 + return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
408 } 397 }
409 398
410 } 399 }
......
1 +<?php namespace BookStack\Repos;
2 +
3 +
4 +use BookStack\Exceptions\PermissionsException;
5 +use BookStack\Permission;
6 +use BookStack\Role;
7 +use Setting;
8 +
9 +class PermissionsRepo
10 +{
11 +
12 + protected $permission;
13 + protected $role;
14 +
15 + /**
16 + * PermissionsRepo constructor.
17 + * @param $permission
18 + * @param $role
19 + */
20 + public function __construct(Permission $permission, Role $role)
21 + {
22 + $this->permission = $permission;
23 + $this->role = $role;
24 + }
25 +
26 + /**
27 + * Get all the user roles from the system.
28 + * @return \Illuminate\Database\Eloquent\Collection|static[]
29 + */
30 + public function getAllRoles()
31 + {
32 + return $this->role->all();
33 + }
34 +
35 + /**
36 + * Get all the roles except for the provided one.
37 + * @param Role $role
38 + * @return mixed
39 + */
40 + public function getAllRolesExcept(Role $role)
41 + {
42 + return $this->role->where('id', '!=', $role->id)->get();
43 + }
44 +
45 + /**
46 + * Get a role via its ID.
47 + * @param $id
48 + * @return mixed
49 + */
50 + public function getRoleById($id)
51 + {
52 + return $this->role->findOrFail($id);
53 + }
54 +
55 + /**
56 + * Save a new role into the system.
57 + * @param array $roleData
58 + * @return Role
59 + */
60 + public function saveNewRole($roleData)
61 + {
62 + $role = $this->role->newInstance($roleData);
63 + $role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
64 + // Prevent duplicate names
65 + while ($this->role->where('name', '=', $role->name)->count() > 0) {
66 + $role->name .= strtolower(str_random(2));
67 + }
68 + $role->save();
69 +
70 + $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
71 + $this->assignRolePermissions($role, $permissions);
72 + return $role;
73 + }
74 +
75 + /**
76 + * Updates an existing role.
77 + * Ensure Admin role always has all permissions.
78 + * @param $roleId
79 + * @param $roleData
80 + */
81 + public function updateRole($roleId, $roleData)
82 + {
83 + $role = $this->role->findOrFail($roleId);
84 + $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
85 + $this->assignRolePermissions($role, $permissions);
86 +
87 + if ($role->name === 'admin') {
88 + $permissions = $this->permission->all()->pluck('id')->toArray();
89 + $role->permissions()->sync($permissions);
90 + }
91 +
92 + $role->fill($roleData);
93 + $role->save();
94 + }
95 +
96 + /**
97 + * Assign an list of permission names to an role.
98 + * @param Role $role
99 + * @param array $permissionNameArray
100 + */
101 + public function assignRolePermissions(Role $role, $permissionNameArray = [])
102 + {
103 + $permissions = [];
104 + $permissionNameArray = array_values($permissionNameArray);
105 + if ($permissionNameArray && count($permissionNameArray) > 0) {
106 + $permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
107 + }
108 + $role->permissions()->sync($permissions);
109 + }
110 +
111 + /**
112 + * Delete a role from the system.
113 + * Check it's not an admin role or set as default before deleting.
114 + * If an migration Role ID is specified the users assign to the current role
115 + * will be added to the role of the specified id.
116 + * @param $roleId
117 + * @param $migrateRoleId
118 + * @throws PermissionsException
119 + */
120 + public function deleteRole($roleId, $migrateRoleId)
121 + {
122 + $role = $this->role->findOrFail($roleId);
123 +
124 + // Prevent deleting admin role or default registration role.
125 + if ($role->name === 'admin') {
126 + throw new PermissionsException('The admin role cannot be deleted');
127 + } else if ($role->id == Setting::get('registration-role')) {
128 + throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
129 + }
130 +
131 + if ($migrateRoleId) {
132 + $newRole = $this->role->find($migrateRoleId);
133 + if ($newRole) {
134 + $users = $role->users->pluck('id')->toArray();
135 + $newRole->users()->sync($users);
136 + }
137 + }
138 +
139 + $role->delete();
140 + }
141 +
142 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -43,6 +43,15 @@ class UserRepo ...@@ -43,6 +43,15 @@ class UserRepo
43 } 43 }
44 44
45 /** 45 /**
46 + * Get all the users with their permissions.
47 + * @return \Illuminate\Database\Eloquent\Builder|static
48 + */
49 + public function getAllUsers()
50 + {
51 + return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
52 + }
53 +
54 + /**
46 * Creates a new user and attaches a role to them. 55 * Creates a new user and attaches a role to them.
47 * @param array $data 56 * @param array $data
48 * @return User 57 * @return User
...@@ -69,7 +78,7 @@ class UserRepo ...@@ -69,7 +78,7 @@ class UserRepo
69 public function attachDefaultRole($user) 78 public function attachDefaultRole($user)
70 { 79 {
71 $roleId = Setting::get('registration-role'); 80 $roleId = Setting::get('registration-role');
72 - if ($roleId === false) $roleId = $this->role->getDefault()->id; 81 + if ($roleId === false) $roleId = $this->role->first()->id;
73 $user->attachRoleId($roleId); 82 $user->attachRoleId($roleId);
74 } 83 }
75 84
...@@ -80,15 +89,10 @@ class UserRepo ...@@ -80,15 +89,10 @@ class UserRepo
80 */ 89 */
81 public function isOnlyAdmin(User $user) 90 public function isOnlyAdmin(User $user)
82 { 91 {
83 - if ($user->role->name != 'admin') { 92 + if (!$user->roles->pluck('name')->contains('admin')) return false;
84 - return false;
85 - }
86 -
87 - $adminRole = $this->role->where('name', '=', 'admin')->first();
88 - if (count($adminRole->users) > 1) {
89 - return false;
90 - }
91 93
94 + $adminRole = $this->role->getRole('admin');
95 + if ($adminRole->users->count() > 1) return false;
92 return true; 96 return true;
93 } 97 }
94 98
...@@ -160,4 +164,14 @@ class UserRepo ...@@ -160,4 +164,14 @@ class UserRepo
160 ]; 164 ];
161 } 165 }
162 166
167 + /**
168 + * Get all the roles which can be given restricted access to
169 + * other entities in the system.
170 + * @return mixed
171 + */
172 + public function getRestrictableRoles()
173 + {
174 + return $this->role->where('name', '!=', 'admin')->get();
175 + }
176 +
163 } 177 }
...\ No newline at end of file ...\ No newline at end of file
......
1 +<?php
2 +
3 +namespace BookStack;
4 +
5 +use Illuminate\Database\Eloquent\Model;
6 +
7 +class Restriction extends Model
8 +{
9 +
10 + protected $fillable = ['role_id', 'action'];
11 + public $timestamps = false;
12 +
13 + /**
14 + * Get all this restriction's attached entity.
15 + * @return \Illuminate\Database\Eloquent\Relations\MorphTo
16 + */
17 + public function restrictable()
18 + {
19 + return $this->morphTo();
20 + }
21 +}
...@@ -6,11 +6,8 @@ use Illuminate\Database\Eloquent\Model; ...@@ -6,11 +6,8 @@ use Illuminate\Database\Eloquent\Model;
6 6
7 class Role extends Model 7 class Role extends Model
8 { 8 {
9 - /** 9 +
10 - * Sets the default role name for newly registered users. 10 + protected $fillable = ['display_name', 'description'];
11 - * @var string
12 - */
13 - protected static $default = 'viewer';
14 11
15 /** 12 /**
16 * The roles that belong to the role. 13 * The roles that belong to the role.
...@@ -29,21 +26,21 @@ class Role extends Model ...@@ -29,21 +26,21 @@ class Role extends Model
29 } 26 }
30 27
31 /** 28 /**
32 - * Add a permission to this role. 29 + * Check if this role has a permission.
33 - * @param Permission $permission 30 + * @param $permission
34 */ 31 */
35 - public function attachPermission(Permission $permission) 32 + public function hasPermission($permission)
36 { 33 {
37 - $this->permissions()->attach($permission->id); 34 + return $this->permissions->pluck('name')->contains($permission);
38 } 35 }
39 36
40 /** 37 /**
41 - * Get an instance of the default role. 38 + * Add a permission to this role.
42 - * @return Role 39 + * @param Permission $permission
43 */ 40 */
44 - public static function getDefault() 41 + public function attachPermission(Permission $permission)
45 { 42 {
46 - return static::getRole(static::$default); 43 + $this->permissions()->attach($permission->id);
47 } 44 }
48 45
49 /** 46 /**
......
1 <?php namespace BookStack\Services; 1 <?php namespace BookStack\Services;
2 2
3 -use Illuminate\Support\Facades\Auth;
4 use BookStack\Activity; 3 use BookStack\Activity;
5 use BookStack\Entity; 4 use BookStack\Entity;
6 use Session; 5 use Session;
...@@ -9,14 +8,17 @@ class ActivityService ...@@ -9,14 +8,17 @@ class ActivityService
9 { 8 {
10 protected $activity; 9 protected $activity;
11 protected $user; 10 protected $user;
11 + protected $restrictionService;
12 12
13 /** 13 /**
14 * ActivityService constructor. 14 * ActivityService constructor.
15 - * @param $activity 15 + * @param Activity $activity
16 + * @param RestrictionService $restrictionService
16 */ 17 */
17 - public function __construct(Activity $activity) 18 + public function __construct(Activity $activity, RestrictionService $restrictionService)
18 { 19 {
19 $this->activity = $activity; 20 $this->activity = $activity;
21 + $this->restrictionService = $restrictionService;
20 $this->user = auth()->user(); 22 $this->user = auth()->user();
21 } 23 }
22 24
...@@ -86,8 +88,10 @@ class ActivityService ...@@ -86,8 +88,10 @@ class ActivityService
86 */ 88 */
87 public function latest($count = 20, $page = 0) 89 public function latest($count = 20, $page = 0)
88 { 90 {
89 - $activityList = $this->activity->orderBy('created_at', 'desc') 91 + $activityList = $this->restrictionService
90 - ->skip($count * $page)->take($count)->get(); 92 + ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
93 + ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
94 +
91 return $this->filterSimilar($activityList); 95 return $this->filterSimilar($activityList);
92 } 96 }
93 97
......
1 +<?php namespace BookStack\Services;
2 +
3 +use BookStack\Entity;
4 +
5 +class RestrictionService
6 +{
7 +
8 + protected $userRoles;
9 + protected $isAdmin;
10 + protected $currentAction;
11 +
12 + /**
13 + * RestrictionService constructor.
14 + */
15 + public function __construct()
16 + {
17 + $user = auth()->user();
18 + $this->userRoles = $user ? auth()->user()->roles->pluck('id') : [];
19 + $this->isAdmin = $user ? auth()->user()->hasRole('admin') : false;
20 + }
21 +
22 + /**
23 + * Checks if an entity has a restriction set upon it.
24 + * @param Entity $entity
25 + * @param $action
26 + * @return bool
27 + */
28 + public function checkIfEntityRestricted(Entity $entity, $action)
29 + {
30 + if ($this->isAdmin) return true;
31 + $this->currentAction = $action;
32 + $baseQuery = $entity->where('id', '=', $entity->id);
33 + if ($entity->isA('page')) {
34 + return $this->pageRestrictionQuery($baseQuery)->count() > 0;
35 + } elseif ($entity->isA('chapter')) {
36 + return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
37 + } elseif ($entity->isA('book')) {
38 + return $this->bookRestrictionQuery($baseQuery)->count() > 0;
39 + }
40 + return false;
41 + }
42 +
43 + /**
44 + * Add restrictions for a page query
45 + * @param $query
46 + * @param string $action
47 + * @return mixed
48 + */
49 + public function enforcePageRestrictions($query, $action = 'view')
50 + {
51 + if ($this->isAdmin) return $query;
52 + $this->currentAction = $action;
53 + return $this->pageRestrictionQuery($query);
54 + }
55 +
56 + /**
57 + * The base query for restricting pages.
58 + * @param $query
59 + * @return mixed
60 + */
61 + private function pageRestrictionQuery($query)
62 + {
63 + return $query->where(function ($parentWhereQuery) {
64 +
65 + $parentWhereQuery
66 + // (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
67 + ->where(function ($query) {
68 + $query->where(function ($query) {
69 + $query->whereExists(function ($query) {
70 + $query->select('*')->from('chapters')
71 + ->whereRaw('chapters.id=pages.chapter_id')
72 + ->where('restricted', '=', false);
73 + })->whereExists(function ($query) {
74 + $query->select('*')->from('books')
75 + ->whereRaw('books.id=pages.book_id')
76 + ->where('restricted', '=', false);
77 + })->where('restricted', '=', false);
78 + })->orWhere(function ($query) {
79 + $query->where('restricted', '=', false)->where('chapter_id', '=', 0)
80 + ->whereExists(function ($query) {
81 + $query->select('*')->from('books')
82 + ->whereRaw('books.id=pages.book_id')
83 + ->where('restricted', '=', false);
84 + });
85 + });
86 + })
87 + // Page unrestricted, Has no chapter & book has accepted restrictions
88 + ->orWhere(function ($query) {
89 + $query->where('restricted', '=', false)
90 + ->whereExists(function ($query) {
91 + $query->select('*')->from('chapters')
92 + ->whereRaw('chapters.id=pages.chapter_id');
93 + }, 'and', true)
94 + ->whereExists(function ($query) {
95 + $query->select('*')->from('books')
96 + ->whereRaw('books.id=pages.book_id')
97 + ->whereExists(function ($query) {
98 + $this->checkRestrictionsQuery($query, 'books', 'Book');
99 + });
100 + });
101 + })
102 + // Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
103 + ->orWhere(function ($query) {
104 + $query->where('restricted', '=', false)
105 + ->whereExists(function ($query) {
106 + $query->select('*')->from('chapters')
107 + ->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
108 + })
109 + ->whereExists(function ($query) {
110 + $query->select('*')->from('books')
111 + ->whereRaw('books.id=pages.book_id')
112 + ->whereExists(function ($query) {
113 + $this->checkRestrictionsQuery($query, 'books', 'Book');
114 + });
115 + });
116 + })
117 + // Page unrestricted, Has a chapter with accepted permissions
118 + ->orWhere(function ($query) {
119 + $query->where('restricted', '=', false)
120 + ->whereExists(function ($query) {
121 + $query->select('*')->from('chapters')
122 + ->whereRaw('chapters.id=pages.chapter_id')
123 + ->where('restricted', '=', true)
124 + ->whereExists(function ($query) {
125 + $this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
126 + });
127 + });
128 + })
129 + // Page has accepted permissions
130 + ->orWhereExists(function ($query) {
131 + $this->checkRestrictionsQuery($query, 'pages', 'Page');
132 + });
133 + });
134 + }
135 +
136 + /**
137 + * Add on permission restrictions to a chapter query.
138 + * @param $query
139 + * @param string $action
140 + * @return mixed
141 + */
142 + public function enforceChapterRestrictions($query, $action = 'view')
143 + {
144 + if ($this->isAdmin) return $query;
145 + $this->currentAction = $action;
146 + return $this->chapterRestrictionQuery($query);
147 + }
148 +
149 + /**
150 + * The base query for restricting chapters.
151 + * @param $query
152 + * @return mixed
153 + */
154 + private function chapterRestrictionQuery($query)
155 + {
156 + return $query->where(function ($parentWhereQuery) {
157 +
158 + $parentWhereQuery
159 + // Book & chapter unrestricted
160 + ->where(function ($query) {
161 + $query->where('restricted', '=', false)->whereExists(function ($query) {
162 + $query->select('*')->from('books')
163 + ->whereRaw('books.id=chapters.book_id')
164 + ->where('restricted', '=', false);
165 + });
166 + })
167 + // Chapter unrestricted & book has accepted restrictions
168 + ->orWhere(function ($query) {
169 + $query->where('restricted', '=', false)
170 + ->whereExists(function ($query) {
171 + $query->select('*')->from('books')
172 + ->whereRaw('books.id=chapters.book_id')
173 + ->whereExists(function ($query) {
174 + $this->checkRestrictionsQuery($query, 'books', 'Book');
175 + });
176 + });
177 + })
178 + // Chapter has accepted permissions
179 + ->orWhereExists(function ($query) {
180 + $this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
181 + });
182 + });
183 + }
184 +
185 + /**
186 + * Add restrictions to a book query.
187 + * @param $query
188 + * @param string $action
189 + * @return mixed
190 + */
191 + public function enforceBookRestrictions($query, $action = 'view')
192 + {
193 + if ($this->isAdmin) return $query;
194 + $this->currentAction = $action;
195 + return $this->bookRestrictionQuery($query);
196 + }
197 +
198 + /**
199 + * The base query for restricting books.
200 + * @param $query
201 + * @return mixed
202 + */
203 + private function bookRestrictionQuery($query)
204 + {
205 + return $query->where(function ($parentWhereQuery) {
206 + $parentWhereQuery
207 + ->where('restricted', '=', false)
208 + ->orWhere(function ($query) {
209 + $query->where('restricted', '=', true)->whereExists(function ($query) {
210 + $this->checkRestrictionsQuery($query, 'books', 'Book');
211 + });
212 + });
213 + });
214 + }
215 +
216 + /**
217 + * Filter items that have entities set a a polymorphic relation.
218 + * @param $query
219 + * @param string $tableName
220 + * @param string $entityIdColumn
221 + * @param string $entityTypeColumn
222 + * @return mixed
223 + */
224 + public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
225 + {
226 + if ($this->isAdmin) return $query;
227 + $this->currentAction = 'view';
228 + $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
229 + return $query->where(function ($query) use ($tableDetails) {
230 + $query->where(function ($query) use (&$tableDetails) {
231 + $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
232 + ->whereExists(function ($query) use (&$tableDetails) {
233 + $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
234 + ->where(function ($query) {
235 + $this->pageRestrictionQuery($query);
236 + });
237 + });
238 + })->orWhere(function ($query) use (&$tableDetails) {
239 + $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
240 + $query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
241 + ->where(function ($query) {
242 + $this->bookRestrictionQuery($query);
243 + });
244 + });
245 + })->orWhere(function ($query) use (&$tableDetails) {
246 + $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
247 + $query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
248 + ->where(function ($query) {
249 + $this->chapterRestrictionQuery($query);
250 + });
251 + });
252 + });
253 + });
254 + }
255 +
256 + /**
257 + * The query to check the restrictions on an entity.
258 + * @param $query
259 + * @param $tableName
260 + * @param $modelName
261 + */
262 + private function checkRestrictionsQuery($query, $tableName, $modelName)
263 + {
264 + $query->select('*')->from('restrictions')
265 + ->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
266 + ->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
267 + ->where('restrictions.action', '=', $this->currentAction)
268 + ->whereIn('restrictions.role_id', $this->userRoles);
269 + }
270 +
271 +
272 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -9,15 +9,18 @@ class ViewService ...@@ -9,15 +9,18 @@ class ViewService
9 9
10 protected $view; 10 protected $view;
11 protected $user; 11 protected $user;
12 + protected $restrictionService;
12 13
13 /** 14 /**
14 * ViewService constructor. 15 * ViewService constructor.
15 - * @param $view 16 + * @param View $view
17 + * @param RestrictionService $restrictionService
16 */ 18 */
17 - public function __construct(View $view) 19 + public function __construct(View $view, RestrictionService $restrictionService)
18 { 20 {
19 $this->view = $view; 21 $this->view = $view;
20 $this->user = auth()->user(); 22 $this->user = auth()->user();
23 + $this->restrictionService = $restrictionService;
21 } 24 }
22 25
23 /** 26 /**
...@@ -27,7 +30,7 @@ class ViewService ...@@ -27,7 +30,7 @@ class ViewService
27 */ 30 */
28 public function add(Entity $entity) 31 public function add(Entity $entity)
29 { 32 {
30 - if($this->user === null) return 0; 33 + if ($this->user === null) return 0;
31 $view = $entity->views()->where('user_id', '=', $this->user->id)->first(); 34 $view = $entity->views()->where('user_id', '=', $this->user->id)->first();
32 // Add view if model exists 35 // Add view if model exists
33 if ($view) { 36 if ($view) {
...@@ -47,18 +50,19 @@ class ViewService ...@@ -47,18 +50,19 @@ class ViewService
47 50
48 /** 51 /**
49 * Get the entities with the most views. 52 * Get the entities with the most views.
50 - * @param int $count 53 + * @param int $count
51 - * @param int $page 54 + * @param int $page
52 * @param bool|false $filterModel 55 * @param bool|false $filterModel
53 */ 56 */
54 public function getPopular($count = 10, $page = 0, $filterModel = false) 57 public function getPopular($count = 10, $page = 0, $filterModel = false)
55 { 58 {
56 $skipCount = $count * $page; 59 $skipCount = $count * $page;
57 - $query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) 60 + $query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
61 + ->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
58 ->groupBy('viewable_id', 'viewable_type') 62 ->groupBy('viewable_id', 'viewable_type')
59 ->orderBy('view_count', 'desc'); 63 ->orderBy('view_count', 'desc');
60 64
61 - if($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); 65 + if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
62 66
63 $views = $query->with('viewable')->skip($skipCount)->take($count)->get(); 67 $views = $query->with('viewable')->skip($skipCount)->take($count)->get();
64 $viewedEntities = $views->map(function ($item) { 68 $viewedEntities = $views->map(function ($item) {
...@@ -69,22 +73,24 @@ class ViewService ...@@ -69,22 +73,24 @@ class ViewService
69 73
70 /** 74 /**
71 * Get all recently viewed entities for the current user. 75 * Get all recently viewed entities for the current user.
72 - * @param int $count 76 + * @param int $count
73 - * @param int $page 77 + * @param int $page
74 * @param Entity|bool $filterModel 78 * @param Entity|bool $filterModel
75 * @return mixed 79 * @return mixed
76 */ 80 */
77 public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) 81 public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
78 { 82 {
79 - if($this->user === null) return collect(); 83 + if ($this->user === null) return collect();
80 $skipCount = $count * $page; 84 $skipCount = $count * $page;
81 - $query = $this->view->where('user_id', '=', auth()->user()->id); 85 + $query = $this->restrictionService
86 + ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
82 87
83 - if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); 88 + if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
89 + $query = $query->where('user_id', '=', auth()->user()->id);
84 90
85 $views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get(); 91 $views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
86 $viewedEntities = $views->map(function ($item) { 92 $viewedEntities = $views->map(function ($item) {
87 - return $item->viewable()->getResults(); 93 + return $item->viewable;
88 }); 94 });
89 return $viewedEntities; 95 return $viewedEntities;
90 } 96 }
......
...@@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
14 14
15 /** 15 /**
16 * The database table used by the model. 16 * The database table used by the model.
17 - *
18 * @var string 17 * @var string
19 */ 18 */
20 protected $table = 'users'; 19 protected $table = 'users';
21 20
22 /** 21 /**
23 * The attributes that are mass assignable. 22 * The attributes that are mass assignable.
24 - *
25 * @var array 23 * @var array
26 */ 24 */
27 protected $fillable = ['name', 'email', 'image_id']; 25 protected $fillable = ['name', 'email', 'image_id'];
28 26
29 /** 27 /**
30 * The attributes excluded from the model's JSON form. 28 * The attributes excluded from the model's JSON form.
31 - *
32 * @var array 29 * @var array
33 */ 30 */
34 protected $hidden = ['password', 'remember_token']; 31 protected $hidden = ['password', 'remember_token'];
...@@ -51,10 +48,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -51,10 +48,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
51 } 48 }
52 49
53 /** 50 /**
54 - * Permissions and roles
55 - */
56 -
57 - /**
58 * The roles that belong to the user. 51 * The roles that belong to the user.
59 */ 52 */
60 public function roles() 53 public function roles()
...@@ -62,21 +55,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -62,21 +55,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
62 return $this->belongsToMany('BookStack\Role'); 55 return $this->belongsToMany('BookStack\Role');
63 } 56 }
64 57
65 - public function getRoleAttribute() 58 + /**
59 + * Check if the user has a role.
60 + * @param $role
61 + * @return mixed
62 + */
63 + public function hasRole($role)
66 { 64 {
67 - return $this->roles()->with('permissions')->first(); 65 + return $this->roles->pluck('name')->contains($role);
68 } 66 }
69 67
70 /** 68 /**
71 - * Loads the user's permissions from their role. 69 + * Get all permissions belonging to a the current user.
70 + * @param bool $cache
71 + * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
72 */ 72 */
73 - private function loadPermissions() 73 + public function permissions($cache = true)
74 { 74 {
75 - if (isset($this->permissions)) return; 75 + if(isset($this->permissions) && $cache) return $this->permissions;
76 $this->load('roles.permissions'); 76 $this->load('roles.permissions');
77 - $permissions = $this->roles[0]->permissions; 77 + $permissions = $this->roles->map(function($role) {
78 - $permissionsArray = $permissions->pluck('name')->all(); 78 + return $role->permissions;
79 - $this->permissions = $permissionsArray; 79 + })->flatten()->unique();
80 + $this->permissions = $permissions;
81 + return $permissions;
80 } 82 }
81 83
82 /** 84 /**
...@@ -86,11 +88,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -86,11 +88,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
86 */ 88 */
87 public function can($permissionName) 89 public function can($permissionName)
88 { 90 {
89 - if ($this->email == 'guest') { 91 + if ($this->email === 'guest') return false;
90 - return false; 92 + return $this->permissions()->pluck('name')->contains($permissionName);
91 - }
92 - $this->loadPermissions();
93 - return array_search($permissionName, $this->permissions) !== false;
94 } 93 }
95 94
96 /** 95 /**
...@@ -108,12 +107,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -108,12 +107,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
108 */ 107 */
109 public function attachRoleId($id) 108 public function attachRoleId($id)
110 { 109 {
111 - $this->roles()->sync([$id]); 110 + $this->roles()->attach($id);
112 } 111 }
113 112
114 /** 113 /**
115 * Get the social account associated with this user. 114 * Get the social account associated with this user.
116 - *
117 * @return \Illuminate\Database\Eloquent\Relations\HasMany 115 * @return \Illuminate\Database\Eloquent\Relations\HasMany
118 */ 116 */
119 public function socialAccounts() 117 public function socialAccounts()
...@@ -138,8 +136,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -138,8 +136,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
138 136
139 /** 137 /**
140 * Returns the user's avatar, 138 * Returns the user's avatar,
141 - * Uses Gravatar as the avatar service.
142 - *
143 * @param int $size 139 * @param int $size
144 * @return string 140 * @return string
145 */ 141 */
......
1 <?php 1 <?php
2 2
3 -if (! function_exists('versioned_asset')) { 3 +if (!function_exists('versioned_asset')) {
4 /** 4 /**
5 * Get the path to a versioned file. 5 * Get the path to a versioned file.
6 * 6 *
7 - * @param string $file 7 + * @param string $file
8 * @return string 8 * @return string
9 * 9 *
10 * @throws \InvalidArgumentException 10 * @throws \InvalidArgumentException
...@@ -27,4 +27,35 @@ if (! function_exists('versioned_asset')) { ...@@ -27,4 +27,35 @@ if (! function_exists('versioned_asset')) {
27 27
28 throw new InvalidArgumentException("File {$file} not defined in asset manifest."); 28 throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
29 } 29 }
30 +}
31 +
32 +/**
33 + * Check if the current user has a permission.
34 + * If an ownable element is passed in the permissions are checked against
35 + * that particular item.
36 + * @param $permission
37 + * @param \BookStack\Ownable $ownable
38 + * @return mixed
39 + */
40 +function userCan($permission, \BookStack\Ownable $ownable = null)
41 +{
42 + if (!auth()->check()) return false;
43 + if ($ownable === null) {
44 + return auth()->user() && auth()->user()->can($permission);
45 + }
46 +
47 + // Check permission on ownable item
48 + $permissionBaseName = strtolower($permission) . '-';
49 + $hasPermission = false;
50 + if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
51 + if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
52 +
53 + if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
54 +
55 + // Check restrictions on the entitiy
56 + $restrictionService = app('BookStack\Services\RestrictionService');
57 + $explodedPermission = explode('-', $permission);
58 + $action = end($explodedPermission);
59 + $hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
60 + return $hasAccess && $hasPermission;
30 } 61 }
...\ No newline at end of file ...\ No newline at end of file
......
1 <?php 1 <?php
2 2
3 +// MEMCACHED - Split out configuration into an array
4 +if (env('CACHE_DRIVER') === 'memcached') {
5 + $memcachedServerKeys = ['host', 'port', 'weight'];
6 + $memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
7 + foreach ($memcachedServers as $index => $memcachedServer) {
8 + $memcachedServerDetails = explode(':', $memcachedServer);
9 + $components = count($memcachedServerDetails);
10 + if ($components < 2) $memcachedServerDetails[] = '11211';
11 + if ($components < 3) $memcachedServerDetails[] = '100';
12 + $memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
13 + }
14 +}
15 +
3 return [ 16 return [
4 17
5 /* 18 /*
...@@ -49,11 +62,7 @@ return [ ...@@ -49,11 +62,7 @@ return [
49 62
50 'memcached' => [ 63 'memcached' => [
51 'driver' => 'memcached', 64 'driver' => 'memcached',
52 - 'servers' => [ 65 + 'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [],
53 - [
54 - 'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
55 - ],
56 - ],
57 ], 66 ],
58 67
59 'redis' => [ 68 'redis' => [
......
...@@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) { ...@@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) {
17 'email' => $faker->email, 17 'email' => $faker->email,
18 'password' => str_random(10), 18 'password' => str_random(10),
19 'remember_token' => str_random(10), 19 'remember_token' => str_random(10),
20 + 'email_confirmed' => 1
20 ]; 21 ];
21 }); 22 });
22 23
...@@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) { ...@@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) {
45 'text' => strip_tags($html) 46 'text' => strip_tags($html)
46 ]; 47 ];
47 }); 48 });
49 +
50 +$factory->define(BookStack\Role::class, function ($faker) {
51 + return [
52 + 'display_name' => $faker->sentence(3),
53 + 'description' => $faker->sentence(10)
54 + ];
55 +});
...\ No newline at end of file ...\ No newline at end of file
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class UpdatePermissionsAndRoles extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + // Get roles with permissions we need to change
16 + $adminRole = \BookStack\Role::getRole('admin');
17 + $editorRole = \BookStack\Role::getRole('editor');
18 +
19 + // Delete old permissions
20 + $permissions = \BookStack\Permission::all();
21 + $permissions->each(function ($permission) {
22 + $permission->delete();
23 + });
24 +
25 + // Create & attach new admin permissions
26 + $permissionsToCreate = [
27 + 'settings-manage' => 'Manage Settings',
28 + 'users-manage' => 'Manage Users',
29 + 'user-roles-manage' => 'Manage Roles & Permissions',
30 + 'restrictions-manage-all' => 'Manage All Entity Restrictions',
31 + 'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content'
32 + ];
33 + foreach ($permissionsToCreate as $name => $displayName) {
34 + $newPermission = new \BookStack\Permission();
35 + $newPermission->name = $name;
36 + $newPermission->display_name = $displayName;
37 + $newPermission->save();
38 + $adminRole->attachPermission($newPermission);
39 + }
40 +
41 + // Create & attach new entity permissions
42 + $entities = ['Book', 'Page', 'Chapter', 'Image'];
43 + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
44 + foreach ($entities as $entity) {
45 + foreach ($ops as $op) {
46 + $newPermission = new \BookStack\Permission();
47 + $newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
48 + $newPermission->display_name = $op . ' ' . $entity . 's';
49 + $newPermission->save();
50 + $adminRole->attachPermission($newPermission);
51 + if ($editorRole !== null) $editorRole->attachPermission($newPermission);
52 + }
53 + }
54 +
55 + }
56 +
57 + /**
58 + * Reverse the migrations.
59 + *
60 + * @return void
61 + */
62 + public function down()
63 + {
64 + // Get roles with permissions we need to change
65 + $adminRole = \BookStack\Role::getRole('admin');
66 +
67 + // Delete old permissions
68 + $permissions = \BookStack\Permission::all();
69 + $permissions->each(function ($permission) {
70 + $permission->delete();
71 + });
72 +
73 + // Create default CRUD permissions and allocate to admins and editors
74 + $entities = ['Book', 'Page', 'Chapter', 'Image'];
75 + $ops = ['Create', 'Update', 'Delete'];
76 + foreach ($entities as $entity) {
77 + foreach ($ops as $op) {
78 + $newPermission = new \BookStack\Permission();
79 + $newPermission->name = strtolower($entity) . '-' . strtolower($op);
80 + $newPermission->display_name = $op . ' ' . $entity . 's';
81 + $newPermission->save();
82 + $adminRole->attachPermission($newPermission);
83 + }
84 + }
85 +
86 + // Create admin permissions
87 + $entities = ['Settings', 'User'];
88 + $ops = ['Create', 'Update', 'Delete'];
89 + foreach ($entities as $entity) {
90 + foreach ($ops as $op) {
91 + $newPermission = new \BookStack\Permission();
92 + $newPermission->name = strtolower($entity) . '-' . strtolower($op);
93 + $newPermission->display_name = $op . ' ' . $entity;
94 + $newPermission->save();
95 + $adminRole->attachPermission($newPermission);
96 + }
97 + }
98 + }
99 +}
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddEntityAccessControls extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('images', function (Blueprint $table) {
16 + $table->integer('uploaded_to')->default(0);
17 + $table->index('uploaded_to');
18 + });
19 +
20 + Schema::table('books', function (Blueprint $table) {
21 + $table->boolean('restricted')->default(false);
22 + $table->index('restricted');
23 + });
24 +
25 + Schema::table('chapters', function (Blueprint $table) {
26 + $table->boolean('restricted')->default(false);
27 + $table->index('restricted');
28 + });
29 +
30 + Schema::table('pages', function (Blueprint $table) {
31 + $table->boolean('restricted')->default(false);
32 + $table->index('restricted');
33 + });
34 +
35 + Schema::create('restrictions', function(Blueprint $table) {
36 + $table->increments('id');
37 + $table->integer('restrictable_id');
38 + $table->string('restrictable_type');
39 + $table->integer('role_id');
40 + $table->string('action');
41 + $table->index('role_id');
42 + $table->index('action');
43 + $table->index(['restrictable_id', 'restrictable_type']);
44 + });
45 + }
46 +
47 + /**
48 + * Reverse the migrations.
49 + *
50 + * @return void
51 + */
52 + public function down()
53 + {
54 + Schema::table('images', function (Blueprint $table) {
55 + $table->dropColumn('uploaded_to');
56 + });
57 +
58 + Schema::table('books', function (Blueprint $table) {
59 + $table->dropColumn('restricted');
60 + });
61 +
62 + Schema::table('chapters', function (Blueprint $table) {
63 + $table->dropColumn('restricted');
64 + });
65 +
66 +
67 + Schema::table('pages', function (Blueprint $table) {
68 + $table->dropColumn('restricted');
69 + });
70 +
71 + Schema::drop('restrictions');
72 + }
73 +}
...@@ -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::getDefault(); 15 + $role = \BookStack\Role::getRole('editor');
16 $user->attachRole($role); 16 $user->attachRole($role);
17 17
18 18
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
21 </filter> 21 </filter>
22 <php> 22 <php>
23 <env name="APP_ENV" value="testing"/> 23 <env name="APP_ENV" value="testing"/>
24 + <env name="APP_DEBUG" value="false"/>
24 <env name="CACHE_DRIVER" value="array"/> 25 <env name="CACHE_DRIVER" value="array"/>
25 <env name="SESSION_DRIVER" value="array"/> 26 <env name="SESSION_DRIVER" value="array"/>
26 <env name="QUEUE_DRIVER" value="sync"/> 27 <env name="QUEUE_DRIVER" value="sync"/>
......
...@@ -118,6 +118,7 @@ module.exports = function (ngApp, events) { ...@@ -118,6 +118,7 @@ module.exports = function (ngApp, events) {
118 page++; 118 page++;
119 }); 119 });
120 } 120 }
121 +
121 $scope.fetchData = fetchData; 122 $scope.fetchData = fetchData;
122 123
123 /** 124 /**
...@@ -130,12 +131,16 @@ module.exports = function (ngApp, events) { ...@@ -130,12 +131,16 @@ module.exports = function (ngApp, events) {
130 $http.put(url, this.selectedImage).then((response) => { 131 $http.put(url, this.selectedImage).then((response) => {
131 events.emit('success', 'Image details updated'); 132 events.emit('success', 'Image details updated');
132 }, (response) => { 133 }, (response) => {
133 - var errors = response.data; 134 + if (response.status === 422) {
134 - var message = ''; 135 + var errors = response.data;
135 - Object.keys(errors).forEach((key) => { 136 + var message = '';
136 - message += errors[key].join('\n'); 137 + Object.keys(errors).forEach((key) => {
137 - }); 138 + message += errors[key].join('\n');
138 - events.emit('error', message); 139 + });
140 + events.emit('error', message);
141 + } else if (response.status === 403) {
142 + events.emit('error', response.data.error);
143 + }
139 }); 144 });
140 }; 145 };
141 146
...@@ -158,6 +163,8 @@ module.exports = function (ngApp, events) { ...@@ -158,6 +163,8 @@ module.exports = function (ngApp, events) {
158 // Pages failure 163 // Pages failure
159 if (response.status === 400) { 164 if (response.status === 400) {
160 $scope.dependantPages = response.data; 165 $scope.dependantPages = response.data;
166 + } else if (response.status === 403) {
167 + events.emit('error', response.data.error);
161 } 168 }
162 }); 169 });
163 }; 170 };
...@@ -167,7 +174,7 @@ module.exports = function (ngApp, events) { ...@@ -167,7 +174,7 @@ module.exports = function (ngApp, events) {
167 * @param stringDate 174 * @param stringDate
168 * @returns {Date} 175 * @returns {Date}
169 */ 176 */
170 - $scope.getDate = function(stringDate) { 177 + $scope.getDate = function (stringDate) {
171 return new Date(stringDate); 178 return new Date(stringDate);
172 }; 179 };
173 180
......
...@@ -87,6 +87,9 @@ header { ...@@ -87,6 +87,9 @@ header {
87 padding-top: $-s; 87 padding-top: $-s;
88 } 88 }
89 } 89 }
90 + .dropdown-container {
91 + font-size: 0.9em;
92 + }
90 } 93 }
91 94
92 form.search-box { 95 form.search-box {
......
...@@ -95,13 +95,14 @@ ...@@ -95,13 +95,14 @@
95 95
96 // Sidebar list 96 // Sidebar list
97 .book-tree { 97 .book-tree {
98 - padding: $-xl 0 0 0; 98 + padding: $-l 0 0 0;
99 position: relative; 99 position: relative;
100 right: 0; 100 right: 0;
101 top: 0; 101 top: 0;
102 transition: ease-in-out 240ms; 102 transition: ease-in-out 240ms;
103 transition-property: right, border; 103 transition-property: right, border;
104 border-left: 0px solid #FFF; 104 border-left: 0px solid #FFF;
105 + background-color: #FFF;
105 &.fixed { 106 &.fixed {
106 position: fixed; 107 position: fixed;
107 top: 0; 108 top: 0;
......
...@@ -8,4 +8,5 @@ return [ ...@@ -8,4 +8,5 @@ return [
8 8
9 // Pages 9 // Pages
10 'permission' => 'You do not have permission to access the requested page.', 10 'permission' => 'You do not have permission to access the requested page.',
11 + 'permissionJson' => 'You do not have permission to perform the requested action.'
11 ]; 12 ];
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
62 <div class="float right"> 62 <div class="float right">
63 <div class="links text-center"> 63 <div class="links text-center">
64 <a href="/books"><i class="zmdi zmdi-book"></i>Books</a> 64 <a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
65 - @if(isset($currentUser) && $currentUser->can('settings-update')) 65 + @if(isset($currentUser) && $currentUser->can('settings-manage'))
66 <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a> 66 <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
67 @endif 67 @endif
68 @if(!isset($signedIn) || !$signedIn) 68 @if(!isset($signedIn) || !$signedIn)
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 <div class="col-xs-1"></div> 8 <div class="col-xs-1"></div>
9 <div class="col-xs-11 faded"> 9 <div class="col-xs-11 faded">
10 <div class="action-buttons"> 10 <div class="action-buttons">
11 - @if($currentUser->can('book-create')) 11 + @if($currentUser->can('book-create-all'))
12 <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a> 12 <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a>
13 @endif 13 @endif
14 </div> 14 </div>
...@@ -30,7 +30,9 @@ ...@@ -30,7 +30,9 @@
30 {!! $books->render() !!} 30 {!! $books->render() !!}
31 @else 31 @else
32 <p class="text-muted">No books have been created.</p> 32 <p class="text-muted">No books have been created.</p>
33 - <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> 33 + @if(userCan('books-create-all'))
34 + <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
35 + @endif
34 @endif 36 @endif
35 </div> 37 </div>
36 <div class="col-sm-4 col-sm-offset-1"> 38 <div class="col-sm-4 col-sm-offset-1">
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="faded-small toolbar">
6 + <div class="container">
7 + <div class="row">
8 + <div class="col-sm-12 faded">
9 + <div class="breadcrumbs">
10 + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
11 + </div>
12 + </div>
13 + </div>
14 + </div>
15 + </div>
16 +
17 +
18 + <div class="container" ng-non-bindable>
19 + <h1>Book Restrictions</h1>
20 + @include('form/restriction-form', ['model' => $book])
21 + </div>
22 +
23 +@stop
...@@ -2,23 +2,35 @@ ...@@ -2,23 +2,35 @@
2 2
3 @section('content') 3 @section('content')
4 4
5 - <div class="faded-small toolbar" ng-non-bindable> 5 + <div class="faded-small toolbar">
6 <div class="container"> 6 <div class="container">
7 <div class="row"> 7 <div class="row">
8 <div class="col-md-12"> 8 <div class="col-md-12">
9 <div class="action-buttons faded"> 9 <div class="action-buttons faded">
10 - @if($currentUser->can('page-create')) 10 + @if(userCan('page-create', $book))
11 <a href="{{$book->getUrl() . '/page/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a> 11 <a href="{{$book->getUrl() . '/page/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a>
12 @endif 12 @endif
13 - @if($currentUser->can('chapter-create')) 13 + @if(userCan('chapter-create', $book))
14 <a href="{{$book->getUrl() . '/chapter/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a> 14 <a href="{{$book->getUrl() . '/chapter/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a>
15 @endif 15 @endif
16 - @if($currentUser->can('book-update')) 16 + @if(userCan('book-update', $book))
17 <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> 17 <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
18 - <a href="{{ $book->getUrl() }}/sort" class="text-primary text-button"><i class="zmdi zmdi-sort"></i>Sort</a>
19 @endif 18 @endif
20 - @if($currentUser->can('book-delete')) 19 + @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
21 - <a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> 20 + <div dropdown class="dropdown-container">
21 + <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
22 + <ul>
23 + @if(userCan('book-update', $book))
24 + <li><a href="{{ $book->getUrl() }}/sort" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li>
25 + @endif
26 + @if(userCan('restrictions-manage', $book))
27 + <li><a href="{{$book->getUrl()}}/restrict" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Restrict</a></li>
28 + @endif
29 + @if(userCan('book-delete', $book))
30 + <li><a href="{{ $book->getUrl() }}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
31 + @endif
32 + </ul>
33 + </div>
22 @endif 34 @endif
23 </div> 35 </div>
24 </div> 36 </div>
...@@ -75,6 +87,15 @@ ...@@ -75,6 +87,15 @@
75 87
76 <div class="col-md-4 col-md-offset-1"> 88 <div class="col-md-4 col-md-offset-1">
77 <div class="margin-top large"></div> 89 <div class="margin-top large"></div>
90 + @if($book->restricted)
91 + <p class="text-muted">
92 + @if(userCan('restrictions-manage', $book))
93 + <a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
94 + @else
95 + <i class="zmdi zmdi-lock-outline"></i>Book Restricted
96 + @endif
97 + </p>
98 + @endif
78 <div class="search-box"> 99 <div class="search-box">
79 <form ng-submit="searchBook($event)"> 100 <form ng-submit="searchBook($event)">
80 <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book"> 101 <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="faded-small toolbar">
6 + <div class="container">
7 + <div class="row">
8 + <div class="col-sm-12 faded">
9 + <div class="breadcrumbs">
10 + <a href="{{$chapter->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
11 + <span class="sep">&raquo;</span>
12 + <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
13 + </div>
14 + </div>
15 + </div>
16 + </div>
17 + </div>
18 +
19 + <div class="container" ng-non-bindable>
20 + <h1>Chapter Restrictions</h1>
21 + @include('form/restriction-form', ['model' => $chapter])
22 + </div>
23 +
24 +@stop
...@@ -12,13 +12,16 @@ ...@@ -12,13 +12,16 @@
12 </div> 12 </div>
13 <div class="col-md-8 faded"> 13 <div class="col-md-8 faded">
14 <div class="action-buttons"> 14 <div class="action-buttons">
15 - @if($currentUser->can('chapter-create')) 15 + @if(userCan('page-create', $chapter))
16 <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a> 16 <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a>
17 @endif 17 @endif
18 - @if($currentUser->can('chapter-update')) 18 + @if(userCan('chapter-update', $chapter))
19 <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> 19 <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
20 @endif 20 @endif
21 - @if($currentUser->can('chapter-delete')) 21 + @if(userCan('restrictions-manage', $chapter))
22 + <a href="{{$chapter->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
23 + @endif
24 + @if(userCan('chapter-delete', $chapter))
22 <a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> 25 <a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
23 @endif 26 @endif
24 </div> 27 </div>
...@@ -34,10 +37,10 @@ ...@@ -34,10 +37,10 @@
34 <h1>{{ $chapter->name }}</h1> 37 <h1>{{ $chapter->name }}</h1>
35 <p class="text-muted">{{ $chapter->description }}</p> 38 <p class="text-muted">{{ $chapter->description }}</p>
36 39
37 - @if(count($chapter->pages) > 0) 40 + @if(count($pages) > 0)
38 <div class="page-list"> 41 <div class="page-list">
39 <hr> 42 <hr>
40 - @foreach($chapter->pages as $page) 43 + @foreach($pages as $page)
41 @include('pages/list-item', ['page' => $page]) 44 @include('pages/list-item', ['page' => $page])
42 <hr> 45 <hr>
43 @endforeach 46 @endforeach
...@@ -60,6 +63,29 @@ ...@@ -60,6 +63,29 @@
60 </p> 63 </p>
61 </div> 64 </div>
62 <div class="col-md-3 col-md-offset-1"> 65 <div class="col-md-3 col-md-offset-1">
66 + <div class="margin-top large"></div>
67 + @if($book->restricted || $chapter->restricted)
68 + <div class="text-muted">
69 +
70 + @if($book->restricted)
71 + @if(userCan('restrictions-manage', $book))
72 + <a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
73 + @else
74 + <i class="zmdi zmdi-lock-outline"></i>Book Restricted
75 + @endif
76 + <br>
77 + @endif
78 +
79 + @if($chapter->restricted)
80 + @if(userCan('restrictions-manage', $chapter))
81 + <a href="{{ $chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter Restricted</a>
82 + @else
83 + <i class="zmdi zmdi-lock-outline"></i>Chapter Restricted
84 + @endif
85 + @endif
86 + </div>
87 + @endif
88 +
63 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) 89 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
64 </div> 90 </div>
65 </div> 91 </div>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 4
5 5
6 <div class="container"> 6 <div class="container">
7 - <h1 class="text-muted">Page Not Found</h1> 7 + <h1 class="text-muted">{{ $message or 'Page Not Found' }}</h1>
8 <p>Sorry, 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 <a href="/" class="button">Return To Home</a>
10 </div> 10 </div>
......
1 +
2 +<label>
3 + <input value="true" id="{{$name}}" type="checkbox" name="{{$name}}"
4 + @if($errors->has($name)) class="neg" @endif
5 + @if(old($name) || (!old() && isset($model) && $model->$name)) checked="checked" @endif
6 + >
7 + {{ $label }}
8 +</label>
9 +
10 +@if($errors->has($name))
11 + <div class="text-neg text-small">{{ $errors->first($name) }}</div>
12 +@endif
...\ No newline at end of file ...\ No newline at end of file
1 +
2 +<label>
3 + <input value="true" id="{{$name}}[{{$role->id}}][{{$action}}]" type="checkbox" name="{{$name}}[{{$role->id}}][{{$action}}]"
4 + @if(old($name .'.'.$role->id.'.'.$action) || (!old() && isset($model) && $model->hasRestriction($role->id, $action))) checked="checked" @endif
5 + >
6 + {{ $label }}
7 +</label>
...\ No newline at end of file ...\ No newline at end of file
1 +<form action="{{ $model->getUrl() }}/restrict" method="POST">
2 + {!! csrf_field() !!}
3 + <input type="hidden" name="_method" value="PUT">
4 +
5 + <div class="form-group">
6 + @include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
7 + </div>
8 +
9 + <table class="table">
10 + <tr>
11 + <th>Role</th>
12 + <th @if($model->isA('page')) colspan="3" @else colspan="4" @endif>Actions</th>
13 + </tr>
14 + @foreach($roles as $role)
15 + <tr>
16 + <td>{{ $role->display_name }}</td>
17 + <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'View', 'action' => 'view'])</td>
18 + @if(!$model->isA('page'))
19 + <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Create', 'action' => 'create'])</td>
20 + @endif
21 + <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Update', 'action' => 'update'])</td>
22 + <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Delete', 'action' => 'delete'])</td>
23 + </tr>
24 + @endforeach
25 + </table>
26 +
27 + <a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
28 + <button type="submit" class="button pos">Save Restrictions</button>
29 +</form>
...\ No newline at end of file ...\ No newline at end of file
1 +
2 +@foreach($roles as $role)
3 + <label>
4 + <input value="{{ $role->id }}" id="{{$name}}-{{$role->name}}" type="checkbox" name="{{$name}}[{{$role->name}}]"
5 + @if($errors->has($name)) class="neg" @endif
6 + @if(old($name . '.' . $role->name) || (!old('name') && isset($model) && $model->hasRole($role->name))) checked="checked" @endif
7 + >
8 + {{ $role->display_name }}
9 + </label>
10 +@endforeach
11 +
12 +@if($errors->has($name))
13 + <div class="text-neg text-small">{{ $errors->first($name) }}</div>
14 +@endif
...\ No newline at end of file ...\ No newline at end of file
...@@ -33,10 +33,14 @@ ...@@ -33,10 +33,14 @@
33 33
34 <div class="col-sm-4"> 34 <div class="col-sm-4">
35 <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3> 35 <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3>
36 - @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) 36 + <div id="recently-created-pages">
37 + @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
38 + </div>
37 39
38 <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3> 40 <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3>
39 - @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) 41 + <div id="recently-updated-pages">
42 + @include('partials/entity-list', ['entities' => $recentlyUpdatedPages, 'style' => 'compact'])
43 + </div>
40 </div> 44 </div>
41 45
42 <div class="col-sm-4" id="recent-activity"> 46 <div class="col-sm-4" id="recent-activity">
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="faded-small toolbar">
6 + <div class="container">
7 + <div class="row">
8 + <div class="col-sm-12 faded">
9 + <div class="breadcrumbs">
10 + <a href="{{$page->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
11 + @if($page->hasChapter())
12 + <span class="sep">&raquo;</span>
13 + <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
14 + <i class="zmdi zmdi-collection-bookmark"></i>
15 + {{$page->chapter->getShortName()}}
16 + </a>
17 + @endif
18 + <span class="sep">&raquo;</span>
19 + <a href="{{$page->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
20 + </div>
21 + </div>
22 + </div>
23 + </div>
24 + </div>
25 +
26 + <div class="container" ng-non-bindable>
27 + <h1>Page Restrictions</h1>
28 + @include('form/restriction-form', ['model' => $page])
29 + </div>
30 +
31 +@stop
...@@ -22,17 +22,20 @@ ...@@ -22,17 +22,20 @@
22 <span dropdown class="dropdown-container"> 22 <span dropdown class="dropdown-container">
23 <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div> 23 <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
24 <ul class="wide"> 24 <ul class="wide">
25 - <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li> 25 + <li><a href="{{$page->getUrl()}}/export/html" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
26 - <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li> 26 + <li><a href="{{$page->getUrl()}}/export/pdf" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
27 - <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li> 27 + <li><a href="{{$page->getUrl()}}/export/plaintext" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
28 </ul> 28 </ul>
29 </span> 29 </span>
30 - @if($currentUser->can('page-update')) 30 + @if(userCan('page-update', $page))
31 - <a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a> 31 + <a href="{{$page->getUrl()}}/revisions" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
32 - <a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a> 32 + <a href="{{$page->getUrl()}}/edit" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
33 @endif 33 @endif
34 - @if($currentUser->can('page-delete')) 34 + @if(userCan('restrictions-manage', $page))
35 - <a href="{{$page->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> 35 + <a href="{{$page->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
36 + @endif
37 + @if(userCan('page-delete', $page))
38 + <a href="{{$page->getUrl()}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
36 @endif 39 @endif
37 </div> 40 </div>
38 </div> 41 </div>
...@@ -67,7 +70,38 @@ ...@@ -67,7 +70,38 @@
67 </div> 70 </div>
68 </div> 71 </div>
69 <div class="col-md-3 print-hidden"> 72 <div class="col-md-3 print-hidden">
73 + <div class="margin-top large"></div>
74 + @if($book->restricted || ($page->chapter && $page->chapter->restricted) || $page->restricted)
75 + <div class="text-muted">
76 +
77 + @if($book->restricted)
78 + @if(userCan('restrictions-manage', $book))
79 + <a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book restricted</a>
80 + @else
81 + <i class="zmdi zmdi-lock-outline"></i>Book restricted
82 + @endif
83 + <br>
84 + @endif
70 85
86 + @if($page->chapter && $page->chapter->restricted)
87 + @if(userCan('restrictions-manage', $page->chapter))
88 + <a href="{{ $page->chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter restricted</a>
89 + @else
90 + <i class="zmdi zmdi-lock-outline"></i>Chapter restricted
91 + @endif
92 + <br>
93 + @endif
94 +
95 + @if($page->restricted)
96 + @if(userCan('restrictions-manage', $page))
97 + <a href="{{ $page->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Page restricted</a>
98 + @else
99 + <i class="zmdi zmdi-lock-outline"></i>Page restricted
100 + @endif
101 + <br>
102 + @endif
103 + </div>
104 + @endif
71 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) 105 @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
72 106
73 </div> 107 </div>
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
16 16
17 {{ $activity->getText() }} 17 {{ $activity->getText() }}
18 18
19 - @if($activity->entity()) 19 + @if($activity->entity)
20 - <a href="{{ $activity->entity()->getUrl() }}">{{ $activity->entity()->name }}</a> 20 + <a href="{{ $activity->entity->getUrl() }}">{{ $activity->entity->name }}</a>
21 @endif 21 @endif
22 22
23 @if($activity->extra) "{{$activity->extra}}" @endif 23 @if($activity->extra) "{{$activity->extra}}" @endif
......
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
59 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif> 59 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
60 @foreach(\BookStack\Role::all() as $role) 60 @foreach(\BookStack\Role::all() as $role)
61 <option value="{{$role->id}}" 61 <option value="{{$role->id}}"
62 - @if(\Setting::get('registration-role', \BookStack\Role::getDefault()->id) == $role->id) selected @endif 62 + @if(\Setting::get('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
63 > 63 >
64 {{ $role->display_name }} 64 {{ $role->display_name }}
65 </option> 65 </option>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 <div class="col-md-12 setting-nav"> 5 <div class="col-md-12 setting-nav">
6 <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a> 6 <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
7 <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a> 7 <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
8 + <a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
8 </div> 9 </div>
9 </div> 10 </div>
10 </div> 11 </div>
......
1 +<input type="checkbox" name="permissions[{{ $permission }}]"
2 + @if(old('permissions.'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission)))) checked="checked" @endif
3 + value="true">
...\ No newline at end of file ...\ No newline at end of file
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + @include('settings/navbar', ['selected' => 'roles'])
6 +
7 + <div class="container">
8 + <h1>Create New Role</h1>
9 +
10 + <form action="/settings/roles/new" method="POST">
11 + @include('settings/roles/form')
12 + </form>
13 + </div>
14 +
15 +@stop
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + @include('settings/navbar', ['selected' => 'roles'])
6 +
7 + <div class="container small" ng-non-bindable>
8 + <h1>Delete Role</h1>
9 + <p>This will delete the role with the name '{{$role->display_name}}'.</p>
10 +
11 + <form action="/settings/roles/delete/{{$role->id}}" method="POST">
12 + {!! csrf_field() !!}
13 + <input type="hidden" name="_method" value="DELETE">
14 +
15 + @if($role->users->count() > 0)
16 + <div class="form-group">
17 + <p>This role has {{$role->users->count()}} users assigned to it. If you would like to migrate the users from this role select a new role below.</p>
18 + @include('form/role-select', ['options' => $roles, 'name' => 'migration_role_id'])
19 + </div>
20 + @endif
21 +
22 + <p class="text-neg">Are you sure you want to delete this role?</p>
23 + <a href="/settings/roles/{{ $role->id }}" class="button">Cancel</a>
24 + <button type="submit" class="button neg">Confirm</button>
25 + </form>
26 + </div>
27 +
28 +@stop
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + @include('settings/navbar', ['selected' => 'roles'])
6 +
7 + <div class="container">
8 + <div class="row">
9 + <div class="col-sm-6">
10 + <h1>Edit Role <small> {{ $role->display_name }}</small></h1>
11 + </div>
12 + <div class="col-sm-6">
13 + <p></p>
14 + <a href="/settings/roles/delete/{{ $role->id }}" class="button neg float right">Delete Role</a>
15 + </div>
16 + </div>
17 +
18 + <form action="/settings/roles/{{ $role->id }}" method="POST">
19 + <input type="hidden" name="_method" value="PUT">
20 + @include('settings/roles/form', ['model' => $role])
21 + </form>
22 + </div>
23 +
24 +@stop
1 +{!! csrf_field() !!}
2 +
3 +<div class="row">
4 +
5 + <div class="col-md-6">
6 + <h3>Role Details</h3>
7 + <div class="form-group">
8 + <label for="name">Role Name</label>
9 + @include('form/text', ['name' => 'display_name'])
10 + </div>
11 + <div class="form-group">
12 + <label for="name">Short Role Description</label>
13 + @include('form/text', ['name' => 'description'])
14 + </div>
15 + <h3>System Permissions</h3>
16 + <div class="row">
17 + <div class="col-md-6">
18 + <label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
19 + </div>
20 + <div class="col-md-6">
21 + <label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage user roles</label>
22 + </div>
23 + </div>
24 + <hr class="even">
25 + <div class="row">
26 + <div class="col-md-6">
27 + <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all restrictions</label>
28 + </div>
29 + <div class="col-md-6">
30 + <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage restrictions on own content</label>
31 + </div>
32 + </div>
33 + <hr class="even">
34 + <div class="form-group">
35 + <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
36 + </div>
37 + <hr class="even">
38 +
39 + </div>
40 +
41 + <div class="col-md-6">
42 +
43 + <h3>Asset Permissions</h3>
44 + <p>
45 + These permissions control default access to the assets within the system. <br>
46 + Restrictions on Books, Chapters and Pages will override these permissions.
47 + </p>
48 + <table class="table">
49 + <tr>
50 + <th></th>
51 + <th>Create</th>
52 + <th>Edit</th>
53 + <th>Delete</th>
54 + </tr>
55 + <tr>
56 + <td>Books</td>
57 + <td>
58 + <label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label>
59 + </td>
60 + <td>
61 + <label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
62 + <label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
63 + </td>
64 + <td>
65 + <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own</label>
66 + <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All</label>
67 + </td>
68 + </tr>
69 + <tr>
70 + <td>Chapters</td>
71 + <td>
72 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) Own</label>
73 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label>
74 + </td>
75 + <td>
76 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
77 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
78 + </td>
79 + <td>
80 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own</label>
81 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All</label>
82 + </td>
83 + </tr>
84 + <tr>
85 + <td>Pages</td>
86 + <td>
87 + <label>@include('settings/roles/checkbox', ['permission' => 'page-create-own']) Own</label>
88 + <label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label>
89 + </td>
90 + <td>
91 + <label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
92 + <label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
93 + </td>
94 + <td>
95 + <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own</label>
96 + <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All</label>
97 + </td>
98 + </tr>
99 + <tr>
100 + <td>Images</td>
101 + <td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
102 + <td>
103 + <label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
104 + <label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
105 + </td>
106 + <td>
107 + <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own</label>
108 + <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
109 + </td>
110 + </tr>
111 + </table>
112 + </div>
113 +
114 +</div>
115 +
116 +<a href="/settings/roles" class="button muted">Cancel</a>
117 +<button type="submit" class="button pos">Save Role</button>
...\ No newline at end of file ...\ No newline at end of file
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + @include('settings/navbar', ['selected' => 'roles'])
6 +
7 + <div class="container small">
8 +
9 + <h1>User Roles</h1>
10 +
11 + <p>
12 + <a href="/settings/roles/new" class="text-pos"><i class="zmdi zmdi-lock-open"></i>Add new role</a>
13 + </p>
14 +
15 + <table class="table">
16 + <tr>
17 + <th>Role Name</th>
18 + <th></th>
19 + <th class="text-right">Users</th>
20 + </tr>
21 + @foreach($roles as $role)
22 + <tr>
23 + <td><a href="/settings/roles/{{ $role->id }}">{{ $role->display_name }}</a></td>
24 + <td>{{ $role->description }}</td>
25 + <td class="text-right">{{ $role->users->count() }}</td>
26 + </tr>
27 + @endforeach
28 + </table>
29 + </div>
30 +
31 +@stop
...@@ -3,21 +3,21 @@ ...@@ -3,21 +3,21 @@
3 @include('form.text', ['name' => 'name']) 3 @include('form.text', ['name' => 'name'])
4 </div> 4 </div>
5 5
6 -@if($currentUser->can('user-update')) 6 +@if(userCan('users-manage'))
7 <div class="form-group"> 7 <div class="form-group">
8 <label for="email">Email</label> 8 <label for="email">Email</label>
9 @include('form.text', ['name' => 'email']) 9 @include('form.text', ['name' => 'email'])
10 </div> 10 </div>
11 @endif 11 @endif
12 12
13 -@if($currentUser->can('user-update')) 13 +@if(userCan('users-manage'))
14 <div class="form-group"> 14 <div class="form-group">
15 <label for="role">User Role</label> 15 <label for="role">User Role</label>
16 - @include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name']) 16 + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()])
17 </div> 17 </div>
18 @endif 18 @endif
19 19
20 -@if($currentUser->can('user-update')) 20 +@if(userCan('users-manage'))
21 <div class="form-group"> 21 <div class="form-group">
22 <label for="external_auth_id">External Authentication ID</label> 22 <label for="external_auth_id">External Authentication ID</label>
23 @include('form.text', ['name' => 'external_auth_id']) 23 @include('form.text', ['name' => 'external_auth_id'])
......
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
8 @include('form.text', ['name' => 'email']) 8 @include('form.text', ['name' => 'email'])
9 </div> 9 </div>
10 10
11 -@if($currentUser->can('user-update')) 11 +@if(userCan('users-manage'))
12 <div class="form-group"> 12 <div class="form-group">
13 <label for="role">User Role</label> 13 <label for="role">User Role</label>
14 - @include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name']) 14 + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()])
15 </div> 15 </div>
16 @endif 16 @endif
17 17
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 8
9 <div class="container small" ng-non-bindable> 9 <div class="container small" ng-non-bindable>
10 <h1>Users</h1> 10 <h1>Users</h1>
11 - @if($currentUser->can('user-create')) 11 + @if(userCan('users-manage'))
12 <p> 12 <p>
13 <a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a> 13 <a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a>
14 </p> 14 </p>
...@@ -18,30 +18,32 @@ ...@@ -18,30 +18,32 @@
18 <th></th> 18 <th></th>
19 <th>Name</th> 19 <th>Name</th>
20 <th>Email</th> 20 <th>Email</th>
21 - <th>User Type</th> 21 + <th>User Roles</th>
22 </tr> 22 </tr>
23 @foreach($users as $user) 23 @foreach($users as $user)
24 <tr> 24 <tr>
25 <td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td> 25 <td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
26 <td> 26 <td>
27 - @if($currentUser->can('user-update') || $currentUser->id == $user->id) 27 + @if(userCan('users-manage') || $currentUser->id == $user->id)
28 <a href="/settings/users/{{$user->id}}"> 28 <a href="/settings/users/{{$user->id}}">
29 @endif 29 @endif
30 {{ $user->name }} 30 {{ $user->name }}
31 - @if($currentUser->can('user-update') || $currentUser->id == $user->id) 31 + @if(userCan('users-manage') || $currentUser->id == $user->id)
32 </a> 32 </a>
33 @endif 33 @endif
34 </td> 34 </td>
35 <td> 35 <td>
36 - @if($currentUser->can('user-update') || $currentUser->id == $user->id) 36 + @if(userCan('users-manage') || $currentUser->id == $user->id)
37 <a href="/settings/users/{{$user->id}}"> 37 <a href="/settings/users/{{$user->id}}">
38 @endif 38 @endif
39 {{ $user->email }} 39 {{ $user->email }}
40 - @if($currentUser->can('user-update') || $currentUser->id == $user->id) 40 + @if(userCan('users-manage') || $currentUser->id == $user->id)
41 </a> 41 </a>
42 @endif 42 @endif
43 </td> 43 </td>
44 - <td>{{ $user->role->display_name }}</td> 44 + <td>
45 + <small> {{ $user->roles->implode('display_name', ', ') }}</small>
46 + </td>
45 </tr> 47 </tr>
46 @endforeach 48 @endforeach
47 </table> 49 </table>
......
...@@ -133,12 +133,12 @@ class AuthTest extends TestCase ...@@ -133,12 +133,12 @@ class AuthTest extends TestCase
133 ->click('Add new user') 133 ->click('Add new user')
134 ->type($user->name, '#name') 134 ->type($user->name, '#name')
135 ->type($user->email, '#email') 135 ->type($user->email, '#email')
136 - ->select(2, '#role') 136 + ->check('roles[admin]')
137 ->type($user->password, '#password') 137 ->type($user->password, '#password')
138 ->type($user->password, '#password-confirm') 138 ->type($user->password, '#password-confirm')
139 ->press('Save') 139 ->press('Save')
140 - ->seeInDatabase('users', $user->toArray())
141 ->seePageIs('/settings/users') 140 ->seePageIs('/settings/users')
141 + ->seeInDatabase('users', $user->toArray())
142 ->see($user->name); 142 ->see($user->name);
143 } 143 }
144 144
......
...@@ -225,4 +225,22 @@ class EntityTest extends TestCase ...@@ -225,4 +225,22 @@ class EntityTest extends TestCase
225 ->seePageIs($newPageUrl); 225 ->seePageIs($newPageUrl);
226 } 226 }
227 227
228 + public function test_recently_updated_pages_on_home()
229 + {
230 + $page = \BookStack\Page::orderBy('updated_at', 'asc')->first();
231 + $this->asAdmin()->visit('/')
232 + ->dontSeeInElement('#recently-updated-pages', $page->name);
233 + $this->visit($page->getUrl() . '/edit')
234 + ->press('Save Page')
235 + ->visit('/')
236 + ->seeInElement('#recently-updated-pages', $page->name);
237 + }
238 +
239 + public function test_recently_created_pages_on_home()
240 + {
241 + $entityChain = $this->createEntityChainBelongingToUser($this->getNewUser());
242 + $this->asAdmin()->visit('/')
243 + ->seeInElement('#recently-created-pages', $entityChain['page']->name);
244 + }
245 +
228 } 246 }
......
1 +<?php
2 +
3 +class RestrictionsTest extends TestCase
4 +{
5 + protected $user;
6 +
7 + public function setUp()
8 + {
9 + parent::setUp();
10 + $this->user = $this->getNewUser();
11 + }
12 +
13 + /**
14 + * Manually set some restrictions on an entity.
15 + * @param \BookStack\Entity $entity
16 + * @param $actions
17 + */
18 + protected function setEntityRestrictions(\BookStack\Entity $entity, $actions)
19 + {
20 + $entity->restricted = true;
21 + $entity->restrictions()->delete();
22 + $role = $this->user->roles->first();
23 + foreach ($actions as $action) {
24 + $entity->restrictions()->create([
25 + 'role_id' => $role->id,
26 + 'action' => strtolower($action)
27 + ]);
28 + }
29 + $entity->save();
30 + $entity->load('restrictions');
31 + }
32 +
33 + public function test_book_view_restriction()
34 + {
35 + $book = \BookStack\Book::first();
36 + $bookPage = $book->pages->first();
37 + $bookChapter = $book->chapters->first();
38 +
39 + $bookUrl = $book->getUrl();
40 + $this->actingAs($this->user)
41 + ->visit($bookUrl)
42 + ->seePageIs($bookUrl);
43 +
44 + $this->setEntityRestrictions($book, []);
45 +
46 + $this->forceVisit($bookUrl)
47 + ->see('Book not found');
48 + $this->forceVisit($bookPage->getUrl())
49 + ->see('Book not found');
50 + $this->forceVisit($bookChapter->getUrl())
51 + ->see('Book not found');
52 +
53 + $this->setEntityRestrictions($book, ['view']);
54 +
55 + $this->visit($bookUrl)
56 + ->see($book->name);
57 + $this->visit($bookPage->getUrl())
58 + ->see($bookPage->name);
59 + $this->visit($bookChapter->getUrl())
60 + ->see($bookChapter->name);
61 + }
62 +
63 + public function test_book_create_restriction()
64 + {
65 + $book = \BookStack\Book::first();
66 +
67 + $bookUrl = $book->getUrl();
68 + $this->actingAs($this->user)
69 + ->visit($bookUrl)
70 + ->seeInElement('.action-buttons', 'New Page')
71 + ->seeInElement('.action-buttons', 'New Chapter');
72 +
73 + $this->setEntityRestrictions($book, ['view', 'delete', 'update']);
74 +
75 + $this->forceVisit($bookUrl . '/chapter/create')
76 + ->see('You do not have permission')->seePageIs('/');
77 + $this->forceVisit($bookUrl . '/page/create')
78 + ->see('You do not have permission')->seePageIs('/');
79 + $this->visit($bookUrl)->dontSeeInElement('.action-buttons', 'New Page')
80 + ->dontSeeInElement('.action-buttons', 'New Chapter');
81 +
82 + $this->setEntityRestrictions($book, ['view', 'create']);
83 +
84 + $this->visit($bookUrl . '/chapter/create')
85 + ->type('test chapter', 'name')
86 + ->type('test description for chapter', 'description')
87 + ->press('Save Chapter')
88 + ->seePageIs($bookUrl . '/chapter/test-chapter');
89 + $this->visit($bookUrl . '/page/create')
90 + ->type('test page', 'name')
91 + ->type('test content', 'html')
92 + ->press('Save Page')
93 + ->seePageIs($bookUrl . '/page/test-page');
94 + $this->visit($bookUrl)->seeInElement('.action-buttons', 'New Page')
95 + ->seeInElement('.action-buttons', 'New Chapter');
96 + }
97 +
98 + public function test_book_update_restriction()
99 + {
100 + $book = \BookStack\Book::first();
101 + $bookPage = $book->pages->first();
102 + $bookChapter = $book->chapters->first();
103 +
104 + $bookUrl = $book->getUrl();
105 + $this->actingAs($this->user)
106 + ->visit($bookUrl . '/edit')
107 + ->see('Edit Book');
108 +
109 + $this->setEntityRestrictions($book, ['view', 'delete']);
110 +
111 + $this->forceVisit($bookUrl . '/edit')
112 + ->see('You do not have permission')->seePageIs('/');
113 + $this->forceVisit($bookPage->getUrl() . '/edit')
114 + ->see('You do not have permission')->seePageIs('/');
115 + $this->forceVisit($bookChapter->getUrl() . '/edit')
116 + ->see('You do not have permission')->seePageIs('/');
117 +
118 + $this->setEntityRestrictions($book, ['view', 'update']);
119 +
120 + $this->visit($bookUrl . '/edit')
121 + ->seePageIs($bookUrl . '/edit');
122 + $this->visit($bookPage->getUrl() . '/edit')
123 + ->seePageIs($bookPage->getUrl() . '/edit');
124 + $this->visit($bookChapter->getUrl() . '/edit')
125 + ->see('Edit Chapter');
126 + }
127 +
128 + public function test_book_delete_restriction()
129 + {
130 + $book = \BookStack\Book::first();
131 + $bookPage = $book->pages->first();
132 + $bookChapter = $book->chapters->first();
133 +
134 + $bookUrl = $book->getUrl();
135 + $this->actingAs($this->user)
136 + ->visit($bookUrl . '/delete')
137 + ->see('Delete Book');
138 +
139 + $this->setEntityRestrictions($book, ['view', 'update']);
140 +
141 + $this->forceVisit($bookUrl . '/delete')
142 + ->see('You do not have permission')->seePageIs('/');
143 + $this->forceVisit($bookPage->getUrl() . '/delete')
144 + ->see('You do not have permission')->seePageIs('/');
145 + $this->forceVisit($bookChapter->getUrl() . '/delete')
146 + ->see('You do not have permission')->seePageIs('/');
147 +
148 + $this->setEntityRestrictions($book, ['view', 'delete']);
149 +
150 + $this->visit($bookUrl . '/delete')
151 + ->seePageIs($bookUrl . '/delete')->see('Delete Book');
152 + $this->visit($bookPage->getUrl() . '/delete')
153 + ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page');
154 + $this->visit($bookChapter->getUrl() . '/delete')
155 + ->see('Delete Chapter');
156 + }
157 +
158 + public function test_chapter_view_restriction()
159 + {
160 + $chapter = \BookStack\Chapter::first();
161 + $chapterPage = $chapter->pages->first();
162 +
163 + $chapterUrl = $chapter->getUrl();
164 + $this->actingAs($this->user)
165 + ->visit($chapterUrl)
166 + ->seePageIs($chapterUrl);
167 +
168 + $this->setEntityRestrictions($chapter, []);
169 +
170 + $this->forceVisit($chapterUrl)
171 + ->see('Chapter not found');
172 + $this->forceVisit($chapterPage->getUrl())
173 + ->see('Page not found');
174 +
175 + $this->setEntityRestrictions($chapter, ['view']);
176 +
177 + $this->visit($chapterUrl)
178 + ->see($chapter->name);
179 + $this->visit($chapterPage->getUrl())
180 + ->see($chapterPage->name);
181 + }
182 +
183 + public function test_chapter_create_restriction()
184 + {
185 + $chapter = \BookStack\Chapter::first();
186 +
187 + $chapterUrl = $chapter->getUrl();
188 + $this->actingAs($this->user)
189 + ->visit($chapterUrl)
190 + ->seeInElement('.action-buttons', 'New Page');
191 +
192 + $this->setEntityRestrictions($chapter, ['view', 'delete', 'update']);
193 +
194 + $this->forceVisit($chapterUrl . '/create-page')
195 + ->see('You do not have permission')->seePageIs('/');
196 + $this->visit($chapterUrl)->dontSeeInElement('.action-buttons', 'New Page');
197 +
198 + $this->setEntityRestrictions($chapter, ['view', 'create']);
199 +
200 +
201 + $this->visit($chapterUrl . '/create-page')
202 + ->type('test page', 'name')
203 + ->type('test content', 'html')
204 + ->press('Save Page')
205 + ->seePageIs($chapter->book->getUrl() . '/page/test-page');
206 + $this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page');
207 + }
208 +
209 + public function test_chapter_update_restriction()
210 + {
211 + $chapter = \BookStack\Chapter::first();
212 + $chapterPage = $chapter->pages->first();
213 +
214 + $chapterUrl = $chapter->getUrl();
215 + $this->actingAs($this->user)
216 + ->visit($chapterUrl . '/edit')
217 + ->see('Edit Chapter');
218 +
219 + $this->setEntityRestrictions($chapter, ['view', 'delete']);
220 +
221 + $this->forceVisit($chapterUrl . '/edit')
222 + ->see('You do not have permission')->seePageIs('/');
223 + $this->forceVisit($chapterPage->getUrl() . '/edit')
224 + ->see('You do not have permission')->seePageIs('/');
225 +
226 + $this->setEntityRestrictions($chapter, ['view', 'update']);
227 +
228 + $this->visit($chapterUrl . '/edit')
229 + ->seePageIs($chapterUrl . '/edit')->see('Edit Chapter');
230 + $this->visit($chapterPage->getUrl() . '/edit')
231 + ->seePageIs($chapterPage->getUrl() . '/edit');
232 + }
233 +
234 + public function test_chapter_delete_restriction()
235 + {
236 + $chapter = \BookStack\Chapter::first();
237 + $chapterPage = $chapter->pages->first();
238 +
239 + $chapterUrl = $chapter->getUrl();
240 + $this->actingAs($this->user)
241 + ->visit($chapterUrl . '/delete')
242 + ->see('Delete Chapter');
243 +
244 + $this->setEntityRestrictions($chapter, ['view', 'update']);
245 +
246 + $this->forceVisit($chapterUrl . '/delete')
247 + ->see('You do not have permission')->seePageIs('/');
248 + $this->forceVisit($chapterPage->getUrl() . '/delete')
249 + ->see('You do not have permission')->seePageIs('/');
250 +
251 + $this->setEntityRestrictions($chapter, ['view', 'delete']);
252 +
253 + $this->visit($chapterUrl . '/delete')
254 + ->seePageIs($chapterUrl . '/delete')->see('Delete Chapter');
255 + $this->visit($chapterPage->getUrl() . '/delete')
256 + ->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page');
257 + }
258 +
259 + public function test_page_view_restriction()
260 + {
261 + $page = \BookStack\Page::first();
262 +
263 + $pageUrl = $page->getUrl();
264 + $this->actingAs($this->user)
265 + ->visit($pageUrl)
266 + ->seePageIs($pageUrl);
267 +
268 + $this->setEntityRestrictions($page, ['update', 'delete']);
269 +
270 + $this->forceVisit($pageUrl)
271 + ->see('Page not found');
272 +
273 + $this->setEntityRestrictions($page, ['view']);
274 +
275 + $this->visit($pageUrl)
276 + ->see($page->name);
277 + }
278 +
279 + public function test_page_update_restriction()
280 + {
281 + $page = \BookStack\Chapter::first();
282 +
283 + $pageUrl = $page->getUrl();
284 + $this->actingAs($this->user)
285 + ->visit($pageUrl . '/edit')
286 + ->seeInField('name', $page->name);
287 +
288 + $this->setEntityRestrictions($page, ['view', 'delete']);
289 +
290 + $this->forceVisit($pageUrl . '/edit')
291 + ->see('You do not have permission')->seePageIs('/');
292 +
293 + $this->setEntityRestrictions($page, ['view', 'update']);
294 +
295 + $this->visit($pageUrl . '/edit')
296 + ->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name);
297 + }
298 +
299 + public function test_page_delete_restriction()
300 + {
301 + $page = \BookStack\Page::first();
302 +
303 + $pageUrl = $page->getUrl();
304 + $this->actingAs($this->user)
305 + ->visit($pageUrl . '/delete')
306 + ->see('Delete Page');
307 +
308 + $this->setEntityRestrictions($page, ['view', 'update']);
309 +
310 + $this->forceVisit($pageUrl . '/delete')
311 + ->see('You do not have permission')->seePageIs('/');
312 +
313 + $this->setEntityRestrictions($page, ['view', 'delete']);
314 +
315 + $this->visit($pageUrl . '/delete')
316 + ->seePageIs($pageUrl . '/delete')->see('Delete Page');
317 + }
318 +
319 + public function test_book_restriction_form()
320 + {
321 + $book = \BookStack\Book::first();
322 + $this->asAdmin()->visit($book->getUrl() . '/restrict')
323 + ->see('Book Restrictions')
324 + ->check('restricted')
325 + ->check('restrictions[2][view]')
326 + ->press('Save Restrictions')
327 + ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
328 + ->seeInDatabase('restrictions', [
329 + 'restrictable_id' => $book->id,
330 + 'restrictable_type' => 'BookStack\Book',
331 + 'role_id' => '2',
332 + 'action' => 'view'
333 + ]);
334 + }
335 +
336 + public function test_chapter_restriction_form()
337 + {
338 + $chapter = \BookStack\Chapter::first();
339 + $this->asAdmin()->visit($chapter->getUrl() . '/restrict')
340 + ->see('Chapter Restrictions')
341 + ->check('restricted')
342 + ->check('restrictions[2][update]')
343 + ->press('Save Restrictions')
344 + ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
345 + ->seeInDatabase('restrictions', [
346 + 'restrictable_id' => $chapter->id,
347 + 'restrictable_type' => 'BookStack\Chapter',
348 + 'role_id' => '2',
349 + 'action' => 'update'
350 + ]);
351 + }
352 +
353 + public function test_page_restriction_form()
354 + {
355 + $page = \BookStack\Page::first();
356 + $this->asAdmin()->visit($page->getUrl() . '/restrict')
357 + ->see('Page Restrictions')
358 + ->check('restricted')
359 + ->check('restrictions[2][delete]')
360 + ->press('Save Restrictions')
361 + ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
362 + ->seeInDatabase('restrictions', [
363 + 'restrictable_id' => $page->id,
364 + 'restrictable_type' => 'BookStack\Page',
365 + 'role_id' => '2',
366 + 'action' => 'delete'
367 + ]);
368 + }
369 +
370 + public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
371 + {
372 + $chapter = \BookStack\Chapter::first();
373 + $page = $chapter->pages->first();
374 + $page2 = $chapter->pages[2];
375 +
376 + $this->setEntityRestrictions($page, []);
377 +
378 + $this->actingAs($this->user)
379 + ->visit($page2->getUrl())
380 + ->dontSeeInElement('.sidebar-page-list', $page->name);
381 + }
382 +
383 + public function test_restricted_pages_not_visible_in_book_navigation_on_chapters()
384 + {
385 + $chapter = \BookStack\Chapter::first();
386 + $page = $chapter->pages->first();
387 +
388 + $this->setEntityRestrictions($page, []);
389 +
390 + $this->actingAs($this->user)
391 + ->visit($chapter->getUrl())
392 + ->dontSeeInElement('.sidebar-page-list', $page->name);
393 + }
394 +
395 + public function test_restricted_pages_not_visible_on_chapter_pages()
396 + {
397 + $chapter = \BookStack\Chapter::first();
398 + $page = $chapter->pages->first();
399 +
400 + $this->setEntityRestrictions($page, []);
401 +
402 + $this->actingAs($this->user)
403 + ->visit($chapter->getUrl())
404 + ->dontSee($page->name);
405 + }
406 +
407 +}
1 +<?php
2 +
3 +class RolesTest extends TestCase
4 +{
5 + protected $user;
6 +
7 + public function setUp()
8 + {
9 + parent::setUp();
10 + $this->user = $this->getNewBlankUser();
11 + }
12 +
13 + /**
14 + * Give the given user some permissions.
15 + * @param \BookStack\User $user
16 + * @param array $permissions
17 + */
18 + protected function giveUserPermissions(\BookStack\User $user, $permissions = [])
19 + {
20 + $newRole = $this->createNewRole($permissions);
21 + $user->attachRole($newRole);
22 + $user->load('roles');
23 + $user->permissions(false);
24 + }
25 +
26 + /**
27 + * Create a new basic role for testing purposes.
28 + * @param array $permissions
29 + * @return static
30 + */
31 + protected function createNewRole($permissions = [])
32 + {
33 + $permissionRepo = app('BookStack\Repos\PermissionsRepo');
34 + $roleData = factory(\BookStack\Role::class)->make()->toArray();
35 + $roleData['permissions'] = array_flip($permissions);
36 + return $permissionRepo->saveNewRole($roleData);
37 + }
38 +
39 + public function test_admin_can_see_settings()
40 + {
41 + $this->asAdmin()->visit('/settings')->see('Settings');
42 + }
43 +
44 + public function test_cannot_delete_admin_role()
45 + {
46 + $adminRole = \BookStack\Role::getRole('admin');
47 + $deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
48 + $this->asAdmin()->visit($deletePageUrl)
49 + ->press('Confirm')
50 + ->seePageIs($deletePageUrl)
51 + ->see('cannot be deleted');
52 + }
53 +
54 + public function test_role_cannot_be_deleted_if_default()
55 + {
56 + $newRole = $this->createNewRole();
57 + $this->setSettings(['registration-role' => $newRole->id]);
58 +
59 + $deletePageUrl = '/settings/roles/delete/' . $newRole->id;
60 + $this->asAdmin()->visit($deletePageUrl)
61 + ->press('Confirm')
62 + ->seePageIs($deletePageUrl)
63 + ->see('cannot be deleted');
64 + }
65 +
66 + public function test_role_create_update_delete_flow()
67 + {
68 + $testRoleName = 'Test Role';
69 + $testRoleDesc = 'a little test description';
70 + $testRoleUpdateName = 'An Super Updated role';
71 +
72 + // Creation
73 + $this->asAdmin()->visit('/settings')
74 + ->click('Roles')
75 + ->seePageIs('/settings/roles')
76 + ->click('Add new role')
77 + ->type('Test Role', 'display_name')
78 + ->type('A little test description', 'description')
79 + ->press('Save Role')
80 + ->seeInDatabase('roles', ['display_name' => $testRoleName, 'name' => 'test-role', 'description' => $testRoleDesc])
81 + ->seePageIs('/settings/roles');
82 + // Updating
83 + $this->asAdmin()->visit('/settings/roles')
84 + ->see($testRoleDesc)
85 + ->click($testRoleName)
86 + ->type($testRoleUpdateName, '#display_name')
87 + ->press('Save Role')
88 + ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'name' => 'test-role', 'description' => $testRoleDesc])
89 + ->seePageIs('/settings/roles');
90 + // Deleting
91 + $this->asAdmin()->visit('/settings/roles')
92 + ->click($testRoleUpdateName)
93 + ->click('Delete Role')
94 + ->see($testRoleUpdateName)
95 + ->press('Confirm')
96 + ->seePageIs('/settings/roles')
97 + ->dontSee($testRoleUpdateName);
98 + }
99 +
100 + public function test_manage_user_permission()
101 + {
102 + $this->actingAs($this->user)->visit('/')->visit('/settings/users')
103 + ->seePageIs('/');
104 + $this->giveUserPermissions($this->user, ['users-manage']);
105 + $this->actingAs($this->user)->visit('/')->visit('/settings/users')
106 + ->seePageIs('/settings/users');
107 + }
108 +
109 + public function test_user_roles_manage_permission()
110 + {
111 + $this->actingAs($this->user)->visit('/')->visit('/settings/roles')
112 + ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/');
113 + $this->giveUserPermissions($this->user, ['user-roles-manage']);
114 + $this->actingAs($this->user)->visit('/settings/roles')
115 + ->seePageIs('/settings/roles')->click('Admin')
116 + ->see('Edit Role');
117 + }
118 +
119 + public function test_settings_manage_permission()
120 + {
121 + $this->actingAs($this->user)->visit('/')->visit('/settings')
122 + ->seePageIs('/');
123 + $this->giveUserPermissions($this->user, ['settings-manage']);
124 + $this->actingAs($this->user)->visit('/')->visit('/settings')
125 + ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved');
126 + }
127 +
128 + public function test_restrictions_manage_all_permission()
129 + {
130 + $page = \BookStack\Page::take(1)->get()->first();
131 + $this->actingAs($this->user)->visit($page->getUrl())
132 + ->dontSee('Restrict')
133 + ->visit($page->getUrl() . '/restrict')
134 + ->seePageIs('/');
135 + $this->giveUserPermissions($this->user, ['restrictions-manage-all']);
136 + $this->actingAs($this->user)->visit($page->getUrl())
137 + ->see('Restrict')
138 + ->click('Restrict')
139 + ->see('Page Restrictions')->seePageIs($page->getUrl() . '/restrict');
140 + }
141 +
142 + public function test_restrictions_manage_own_permission()
143 + {
144 + $otherUsersPage = \BookStack\Page::take(1)->get()->first();
145 + $content = $this->createEntityChainBelongingToUser($this->user);
146 + // Check can't restrict other's content
147 + $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
148 + ->dontSee('Restrict')
149 + ->visit($otherUsersPage->getUrl() . '/restrict')
150 + ->seePageIs('/');
151 + // Check can't restrict own content
152 + $this->actingAs($this->user)->visit($content['page']->getUrl())
153 + ->dontSee('Restrict')
154 + ->visit($content['page']->getUrl() . '/restrict')
155 + ->seePageIs('/');
156 +
157 + $this->giveUserPermissions($this->user, ['restrictions-manage-own']);
158 +
159 + // Check can't restrict other's content
160 + $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
161 + ->dontSee('Restrict')
162 + ->visit($otherUsersPage->getUrl() . '/restrict')
163 + ->seePageIs('/');
164 + // Check can restrict own content
165 + $this->actingAs($this->user)->visit($content['page']->getUrl())
166 + ->see('Restrict')
167 + ->click('Restrict')
168 + ->seePageIs($content['page']->getUrl() . '/restrict');
169 + }
170 +
171 + /**
172 + * Check a standard entity access permission
173 + * @param string $permission
174 + * @param array $accessUrls Urls that are only accessible after having the permission
175 + * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission
176 + * @param null $callback
177 + */
178 + private function checkAccessPermission($permission, $accessUrls = [], $visibles = [])
179 + {
180 + foreach ($accessUrls as $url) {
181 + $this->actingAs($this->user)->visit('/')->visit($url)
182 + ->seePageIs('/');
183 + }
184 + foreach ($visibles as $url => $text) {
185 + $this->actingAs($this->user)->visit('/')->visit($url)
186 + ->dontSeeInElement('.action-buttons',$text);
187 + }
188 +
189 + $this->giveUserPermissions($this->user, [$permission]);
190 +
191 + foreach ($accessUrls as $url) {
192 + $this->actingAs($this->user)->visit('/')->visit($url)
193 + ->seePageIs($url);
194 + }
195 + foreach ($visibles as $url => $text) {
196 + $this->actingAs($this->user)->visit('/')->visit($url)
197 + ->see($text);
198 + }
199 + }
200 +
201 + public function test_books_create_all_permissions()
202 + {
203 + $this->checkAccessPermission('book-create-all', [
204 + '/books/create'
205 + ], [
206 + '/books' => 'Add new book'
207 + ]);
208 +
209 + $this->visit('/books/create')
210 + ->type('test book', 'name')
211 + ->type('book desc', 'description')
212 + ->press('Save Book')
213 + ->seePageIs('/books/test-book');
214 + }
215 +
216 + public function test_books_edit_own_permission()
217 + {
218 + $otherBook = \BookStack\Book::take(1)->get()->first();
219 + $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
220 + $this->checkAccessPermission('book-update-own', [
221 + $ownBook->getUrl() . '/edit'
222 + ], [
223 + $ownBook->getUrl() => 'Edit'
224 + ]);
225 +
226 + $this->visit($otherBook->getUrl())
227 + ->dontSeeInElement('.action-buttons', 'Edit')
228 + ->visit($otherBook->getUrl() . '/edit')
229 + ->seePageIs('/');
230 + }
231 +
232 + public function test_books_edit_all_permission()
233 + {
234 + $otherBook = \BookStack\Book::take(1)->get()->first();
235 + $this->checkAccessPermission('book-update-all', [
236 + $otherBook->getUrl() . '/edit'
237 + ], [
238 + $otherBook->getUrl() => 'Edit'
239 + ]);
240 + }
241 +
242 + public function test_books_delete_own_permission()
243 + {
244 + $this->giveUserPermissions($this->user, ['book-update-all']);
245 + $otherBook = \BookStack\Book::take(1)->get()->first();
246 + $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
247 + $this->checkAccessPermission('book-delete-own', [
248 + $ownBook->getUrl() . '/delete'
249 + ], [
250 + $ownBook->getUrl() => 'Delete'
251 + ]);
252 +
253 + $this->visit($otherBook->getUrl())
254 + ->dontSeeInElement('.action-buttons', 'Delete')
255 + ->visit($otherBook->getUrl() . '/delete')
256 + ->seePageIs('/');
257 + $this->visit($ownBook->getUrl())->visit($ownBook->getUrl() . '/delete')
258 + ->press('Confirm')
259 + ->seePageIs('/books')
260 + ->dontSee($ownBook->name);
261 + }
262 +
263 + public function test_books_delete_all_permission()
264 + {
265 + $this->giveUserPermissions($this->user, ['book-update-all']);
266 + $otherBook = \BookStack\Book::take(1)->get()->first();
267 + $this->checkAccessPermission('book-delete-all', [
268 + $otherBook->getUrl() . '/delete'
269 + ], [
270 + $otherBook->getUrl() => 'Delete'
271 + ]);
272 +
273 + $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete')
274 + ->press('Confirm')
275 + ->seePageIs('/books')
276 + ->dontSee($otherBook->name);
277 + }
278 +
279 + public function test_chapter_create_own_permissions()
280 + {
281 + $book = \BookStack\Book::take(1)->get()->first();
282 + $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
283 + $baseUrl = $ownBook->getUrl() . '/chapter';
284 + $this->checkAccessPermission('chapter-create-own', [
285 + $baseUrl . '/create'
286 + ], [
287 + $ownBook->getUrl() => 'New Chapter'
288 + ]);
289 +
290 + $this->visit($baseUrl . '/create')
291 + ->type('test chapter', 'name')
292 + ->type('chapter desc', 'description')
293 + ->press('Save Chapter')
294 + ->seePageIs($baseUrl . '/test-chapter');
295 +
296 + $this->visit($book->getUrl())
297 + ->dontSeeInElement('.action-buttons', 'New Chapter')
298 + ->visit($book->getUrl() . '/chapter/create')
299 + ->seePageIs('/');
300 + }
301 +
302 + public function test_chapter_create_all_permissions()
303 + {
304 + $book = \BookStack\Book::take(1)->get()->first();
305 + $baseUrl = $book->getUrl() . '/chapter';
306 + $this->checkAccessPermission('chapter-create-all', [
307 + $baseUrl . '/create'
308 + ], [
309 + $book->getUrl() => 'New Chapter'
310 + ]);
311 +
312 + $this->visit($baseUrl . '/create')
313 + ->type('test chapter', 'name')
314 + ->type('chapter desc', 'description')
315 + ->press('Save Chapter')
316 + ->seePageIs($baseUrl . '/test-chapter');
317 + }
318 +
319 + public function test_chapter_edit_own_permission()
320 + {
321 + $otherChapter = \BookStack\Chapter::take(1)->get()->first();
322 + $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
323 + $this->checkAccessPermission('chapter-update-own', [
324 + $ownChapter->getUrl() . '/edit'
325 + ], [
326 + $ownChapter->getUrl() => 'Edit'
327 + ]);
328 +
329 + $this->visit($otherChapter->getUrl())
330 + ->dontSeeInElement('.action-buttons', 'Edit')
331 + ->visit($otherChapter->getUrl() . '/edit')
332 + ->seePageIs('/');
333 + }
334 +
335 + public function test_chapter_edit_all_permission()
336 + {
337 + $otherChapter = \BookStack\Chapter::take(1)->get()->first();
338 + $this->checkAccessPermission('chapter-update-all', [
339 + $otherChapter->getUrl() . '/edit'
340 + ], [
341 + $otherChapter->getUrl() => 'Edit'
342 + ]);
343 + }
344 +
345 + public function test_chapter_delete_own_permission()
346 + {
347 + $this->giveUserPermissions($this->user, ['chapter-update-all']);
348 + $otherChapter = \BookStack\Chapter::take(1)->get()->first();
349 + $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
350 + $this->checkAccessPermission('chapter-delete-own', [
351 + $ownChapter->getUrl() . '/delete'
352 + ], [
353 + $ownChapter->getUrl() => 'Delete'
354 + ]);
355 +
356 + $bookUrl = $ownChapter->book->getUrl();
357 + $this->visit($otherChapter->getUrl())
358 + ->dontSeeInElement('.action-buttons', 'Delete')
359 + ->visit($otherChapter->getUrl() . '/delete')
360 + ->seePageIs('/');
361 + $this->visit($ownChapter->getUrl())->visit($ownChapter->getUrl() . '/delete')
362 + ->press('Confirm')
363 + ->seePageIs($bookUrl)
364 + ->dontSeeInElement('.book-content', $ownChapter->name);
365 + }
366 +
367 + public function test_chapter_delete_all_permission()
368 + {
369 + $this->giveUserPermissions($this->user, ['chapter-update-all']);
370 + $otherChapter = \BookStack\Chapter::take(1)->get()->first();
371 + $this->checkAccessPermission('chapter-delete-all', [
372 + $otherChapter->getUrl() . '/delete'
373 + ], [
374 + $otherChapter->getUrl() => 'Delete'
375 + ]);
376 +
377 + $bookUrl = $otherChapter->book->getUrl();
378 + $this->visit($otherChapter->getUrl())->visit($otherChapter->getUrl() . '/delete')
379 + ->press('Confirm')
380 + ->seePageIs($bookUrl)
381 + ->dontSeeInElement('.book-content', $otherChapter->name);
382 + }
383 +
384 + public function test_page_create_own_permissions()
385 + {
386 + $book = \BookStack\Book::take(1)->get()->first();
387 + $chapter = \BookStack\Chapter::take(1)->get()->first();
388 +
389 + $entities = $this->createEntityChainBelongingToUser($this->user);
390 + $ownBook = $entities['book'];
391 + $ownChapter = $entities['chapter'];
392 +
393 + $baseUrl = $ownBook->getUrl() . '/page';
394 +
395 + $this->checkAccessPermission('page-create-own', [
396 + $baseUrl . '/create',
397 + $ownChapter->getUrl() . '/create-page'
398 + ], [
399 + $ownBook->getUrl() => 'New Page',
400 + $ownChapter->getUrl() => 'New Page'
401 + ]);
402 +
403 + $this->visit($baseUrl . '/create')
404 + ->type('test page', 'name')
405 + ->type('page desc', 'html')
406 + ->press('Save Page')
407 + ->seePageIs($baseUrl . '/test-page');
408 +
409 + $this->visit($book->getUrl())
410 + ->dontSeeInElement('.action-buttons', 'New Page')
411 + ->visit($book->getUrl() . '/page/create')
412 + ->seePageIs('/');
413 + $this->visit($chapter->getUrl())
414 + ->dontSeeInElement('.action-buttons', 'New Page')
415 + ->visit($chapter->getUrl() . '/create-page')
416 + ->seePageIs('/');
417 + }
418 +
419 + public function test_page_create_all_permissions()
420 + {
421 + $book = \BookStack\Book::take(1)->get()->first();
422 + $chapter = \BookStack\Chapter::take(1)->get()->first();
423 + $baseUrl = $book->getUrl() . '/page';
424 + $this->checkAccessPermission('page-create-all', [
425 + $baseUrl . '/create',
426 + $chapter->getUrl() . '/create-page'
427 + ], [
428 + $book->getUrl() => 'New Page',
429 + $chapter->getUrl() => 'New Page'
430 + ]);
431 +
432 + $this->visit($baseUrl . '/create')
433 + ->type('test page', 'name')
434 + ->type('page desc', 'html')
435 + ->press('Save Page')
436 + ->seePageIs($baseUrl . '/test-page');
437 +
438 + $this->visit($chapter->getUrl() . '/create-page')
439 + ->type('new test page', 'name')
440 + ->type('page desc', 'html')
441 + ->press('Save Page')
442 + ->seePageIs($baseUrl . '/new-test-page');
443 + }
444 +
445 + public function test_page_edit_own_permission()
446 + {
447 + $otherPage = \BookStack\Page::take(1)->get()->first();
448 + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
449 + $this->checkAccessPermission('page-update-own', [
450 + $ownPage->getUrl() . '/edit'
451 + ], [
452 + $ownPage->getUrl() => 'Edit'
453 + ]);
454 +
455 + $this->visit($otherPage->getUrl())
456 + ->dontSeeInElement('.action-buttons', 'Edit')
457 + ->visit($otherPage->getUrl() . '/edit')
458 + ->seePageIs('/');
459 + }
460 +
461 + public function test_page_edit_all_permission()
462 + {
463 + $otherPage = \BookStack\Page::take(1)->get()->first();
464 + $this->checkAccessPermission('page-update-all', [
465 + $otherPage->getUrl() . '/edit'
466 + ], [
467 + $otherPage->getUrl() => 'Edit'
468 + ]);
469 + }
470 +
471 + public function test_page_delete_own_permission()
472 + {
473 + $this->giveUserPermissions($this->user, ['page-update-all']);
474 + $otherPage = \BookStack\Page::take(1)->get()->first();
475 + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
476 + $this->checkAccessPermission('page-delete-own', [
477 + $ownPage->getUrl() . '/delete'
478 + ], [
479 + $ownPage->getUrl() => 'Delete'
480 + ]);
481 +
482 + $bookUrl = $ownPage->book->getUrl();
483 + $this->visit($otherPage->getUrl())
484 + ->dontSeeInElement('.action-buttons', 'Delete')
485 + ->visit($otherPage->getUrl() . '/delete')
486 + ->seePageIs('/');
487 + $this->visit($ownPage->getUrl())->visit($ownPage->getUrl() . '/delete')
488 + ->press('Confirm')
489 + ->seePageIs($bookUrl)
490 + ->dontSeeInElement('.book-content', $ownPage->name);
491 + }
492 +
493 + public function test_page_delete_all_permission()
494 + {
495 + $this->giveUserPermissions($this->user, ['page-update-all']);
496 + $otherPage = \BookStack\Page::take(1)->get()->first();
497 + $this->checkAccessPermission('page-delete-all', [
498 + $otherPage->getUrl() . '/delete'
499 + ], [
500 + $otherPage->getUrl() => 'Delete'
501 + ]);
502 +
503 + $bookUrl = $otherPage->book->getUrl();
504 + $this->visit($otherPage->getUrl())->visit($otherPage->getUrl() . '/delete')
505 + ->press('Confirm')
506 + ->seePageIs($bookUrl)
507 + ->dontSeeInElement('.book-content', $otherPage->name);
508 + }
509 +
510 +}
1 <?php 1 <?php
2 2
3 use Illuminate\Foundation\Testing\DatabaseTransactions; 3 use Illuminate\Foundation\Testing\DatabaseTransactions;
4 +use Symfony\Component\DomCrawler\Crawler;
4 5
5 class TestCase extends Illuminate\Foundation\Testing\TestCase 6 class TestCase extends Illuminate\Foundation\Testing\TestCase
6 { 7 {
...@@ -32,7 +33,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -32,7 +33,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
32 public function asAdmin() 33 public function asAdmin()
33 { 34 {
34 if($this->admin === null) { 35 if($this->admin === null) {
35 - $this->admin = \BookStack\User::find(1); 36 + $adminRole = \BookStack\Role::getRole('admin');
37 + $this->admin = $adminRole->users->first();
36 } 38 }
37 return $this->actingAs($this->admin); 39 return $this->actingAs($this->admin);
38 } 40 }
...@@ -78,8 +80,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -78,8 +80,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
78 protected function getNewUser($attributes = []) 80 protected function getNewUser($attributes = [])
79 { 81 {
80 $user = factory(\BookStack\User::class)->create($attributes); 82 $user = factory(\BookStack\User::class)->create($attributes);
81 - $userRepo = app('BookStack\Repos\UserRepo'); 83 + $role = \BookStack\Role::getRole('editor');
82 - $userRepo->attachDefaultRole($user); 84 + $user->attachRole($role);;
85 + return $user;
86 + }
87 +
88 + /**
89 + * Quick way to create a new user without any permissions
90 + * @param array $attributes
91 + * @return mixed
92 + */
93 + protected function getNewBlankUser($attributes = [])
94 + {
95 + $user = factory(\BookStack\User::class)->create($attributes);
83 return $user; 96 return $user;
84 } 97 }
85 98
...@@ -111,6 +124,40 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -111,6 +124,40 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
111 } 124 }
112 125
113 /** 126 /**
127 + * Assert that the current page matches a given URI.
128 + *
129 + * @param string $uri
130 + * @return $this
131 + */
132 + protected function seePageUrlIs($uri)
133 + {
134 + $this->assertEquals(
135 + $uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
136 + );
137 +
138 + return $this;
139 + }
140 +
141 + /**
142 + * Do a forced visit that does not error out on exception.
143 + * @param string $uri
144 + * @param array $parameters
145 + * @param array $cookies
146 + * @param array $files
147 + * @return $this
148 + */
149 + protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
150 + {
151 + $method = 'GET';
152 + $uri = $this->prepareUrlForRequest($uri);
153 + $this->call($method, $uri, $parameters, $cookies, $files);
154 + $this->clearInputs()->followRedirects();
155 + $this->currentUri = $this->app->make('request')->fullUrl();
156 + $this->crawler = new Crawler($this->response->getContent(), $uri);
157 + return $this;
158 + }
159 +
160 + /**
114 * Click the text within the selected element. 161 * Click the text within the selected element.
115 * @param $parentElement 162 * @param $parentElement
116 * @param $linkText 163 * @param $linkText
......