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); | ... | ... |
app/Exceptions/NotFoundException.php
0 → 100644
| 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 |
app/Exceptions/PermissionsException.php
0 → 100644
| ... | @@ -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 | } | ... | ... |
app/Repos/PermissionsRepo.php
0 → 100644
| 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 | ... | ... |
app/Restriction.php
0 → 100644
| 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 | ... | ... |
app/Services/RestrictionService.php
0 → 100644
| 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 | ... | ... |
| ... | @@ -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"> | ... | ... |
resources/views/books/restrictions.blade.php
0 → 100644
| 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">»</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> | ... | ... |
resources/views/form/checkbox.blade.php
0 → 100644
| 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"> | ... | ... |
resources/views/pages/restrictions.blade.php
0 → 100644
| 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">»</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">»</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 | } | ... | ... |
tests/RestrictionsTest.php
0 → 100644
| 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 | +} |
tests/RolesTest.php
0 → 100644
| 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 | ... | ... |
-
Please register or sign in to post a comment