Dan Brown

Merge branch 'master' into release for version v0.10

Showing 99 changed files with 2281 additions and 442 deletions
1 +language: php
2 +php:
3 + - 7.0
4 +
5 +cache:
6 + directories:
7 + - vendor
8 +
9 +addons:
10 + mariadb: '10.0'
11 +
12 +before_install:
13 + - npm install -g npm@latest
14 +
15 +before_script:
16 + - mysql -e 'create database `bookstack-test`;'
17 + - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
18 + - phpenv config-rm xdebug.ini
19 + - composer self-update
20 + - composer install --prefer-dist --no-interaction
21 + - npm install
22 + - ./node_modules/.bin/gulp
23 + - php artisan migrate --force -n --database=mysql_testing
24 + - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
25 +
26 +script:
27 + - vendor/bin/phpunit
...\ No newline at end of file ...\ No newline at end of file
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
2 2
3 namespace BookStack; 3 namespace BookStack;
4 4
5 -use Illuminate\Database\Eloquent\Model;
6 -
7 /** 5 /**
8 * @property string key 6 * @property string key
9 * @property \User user 7 * @property \User user
...@@ -28,7 +26,7 @@ class Activity extends Model ...@@ -28,7 +26,7 @@ class Activity extends Model
28 */ 26 */
29 public function user() 27 public function user()
30 { 28 {
31 - return $this->belongsTo('BookStack\User'); 29 + return $this->belongsTo(User::class);
32 } 30 }
33 31
34 /** 32 /**
......
1 -<?php 1 +<?php namespace BookStack;
2 -
3 -namespace BookStack;
4 2
5 class Book extends Entity 3 class Book extends Entity
6 { 4 {
7 5
8 protected $fillable = ['name', 'description']; 6 protected $fillable = ['name', 'description'];
9 7
8 + /**
9 + * Get the url for this book.
10 + * @return string
11 + */
10 public function getUrl() 12 public function getUrl()
11 { 13 {
12 return '/books/' . $this->slug; 14 return '/books/' . $this->slug;
13 } 15 }
14 16
17 + /*
18 + * Get the edit url for this book.
19 + * @return string
20 + */
15 public function getEditUrl() 21 public function getEditUrl()
16 { 22 {
17 return $this->getUrl() . '/edit'; 23 return $this->getUrl() . '/edit';
18 } 24 }
19 25
26 + /**
27 + * Get all pages within this book.
28 + * @return \Illuminate\Database\Eloquent\Relations\HasMany
29 + */
20 public function pages() 30 public function pages()
21 { 31 {
22 - return $this->hasMany('BookStack\Page'); 32 + return $this->hasMany(Page::class);
23 } 33 }
24 34
35 + /**
36 + * Get all chapters within this book.
37 + * @return \Illuminate\Database\Eloquent\Relations\HasMany
38 + */
25 public function chapters() 39 public function chapters()
26 { 40 {
27 - return $this->hasMany('BookStack\Chapter'); 41 + return $this->hasMany(Chapter::class);
28 } 42 }
29 43
44 + /**
45 + * Get an excerpt of this book's description to the specified length or less.
46 + * @param int $length
47 + * @return string
48 + */
30 public function getExcerpt($length = 100) 49 public function getExcerpt($length = 100)
31 { 50 {
32 - return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description; 51 + $description = $this->description;
52 + return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
33 } 53 }
34 54
35 } 55 }
......
...@@ -5,25 +5,43 @@ class Chapter extends Entity ...@@ -5,25 +5,43 @@ class Chapter extends Entity
5 { 5 {
6 protected $fillable = ['name', 'description', 'priority', 'book_id']; 6 protected $fillable = ['name', 'description', 'priority', 'book_id'];
7 7
8 + /**
9 + * Get the book this chapter is within.
10 + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
11 + */
8 public function book() 12 public function book()
9 { 13 {
10 - return $this->belongsTo('BookStack\Book'); 14 + return $this->belongsTo(Book::class);
11 } 15 }
12 16
17 + /**
18 + * Get the pages that this chapter contains.
19 + * @return mixed
20 + */
13 public function pages() 21 public function pages()
14 { 22 {
15 - return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC'); 23 + return $this->hasMany(Page::class)->orderBy('priority', 'ASC');
16 } 24 }
17 25
26 + /**
27 + * Get the url of this chapter.
28 + * @return string
29 + */
18 public function getUrl() 30 public function getUrl()
19 { 31 {
20 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; 32 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
21 return '/books/' . $bookSlug. '/chapter/' . $this->slug; 33 return '/books/' . $bookSlug. '/chapter/' . $this->slug;
22 } 34 }
23 35
36 + /**
37 + * Get an excerpt of this chapter's description to the specified length or less.
38 + * @param int $length
39 + * @return string
40 + */
24 public function getExcerpt($length = 100) 41 public function getExcerpt($length = 100)
25 { 42 {
26 - return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description; 43 + $description = $this->description;
44 + return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
27 } 45 }
28 46
29 } 47 }
......
1 +<?php
2 +
3 +namespace BookStack\Console\Commands;
4 +
5 +use BookStack\Services\PermissionService;
6 +use Illuminate\Console\Command;
7 +
8 +class RegeneratePermissions extends Command
9 +{
10 + /**
11 + * The name and signature of the console command.
12 + *
13 + * @var string
14 + */
15 + protected $signature = 'permissions:regen';
16 +
17 + /**
18 + * The console command description.
19 + *
20 + * @var string
21 + */
22 + protected $description = 'Regenerate all system permissions';
23 +
24 + /**
25 + * The service to handle the permission system.
26 + *
27 + * @var PermissionService
28 + */
29 + protected $permissionService;
30 +
31 + /**
32 + * Create a new command instance.
33 + *
34 + * @param PermissionService $permissionService
35 + */
36 + public function __construct(PermissionService $permissionService)
37 + {
38 + $this->permissionService = $permissionService;
39 + parent::__construct();
40 + }
41 +
42 + /**
43 + * Execute the console command.
44 + *
45 + * @return mixed
46 + */
47 + public function handle()
48 + {
49 + $this->permissionService->buildJointPermissions();
50 + }
51 +}
...@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel ...@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
15 protected $commands = [ 15 protected $commands = [
16 \BookStack\Console\Commands\Inspire::class, 16 \BookStack\Console\Commands\Inspire::class,
17 \BookStack\Console\Commands\ResetViews::class, 17 \BookStack\Console\Commands\ResetViews::class,
18 + \BookStack\Console\Commands\RegeneratePermissions::class,
18 ]; 19 ];
19 20
20 /** 21 /**
......
1 -<?php 1 +<?php namespace BookStack;
2 -
3 -namespace BookStack;
4 -
5 -use Illuminate\Database\Eloquent\Model;
6 2
7 class EmailConfirmation extends Model 3 class EmailConfirmation extends Model
8 { 4 {
9 protected $fillable = ['user_id', 'token']; 5 protected $fillable = ['user_id', 'token'];
10 6
7 + /**
8 + * Get the user that this confirmation is attached to.
9 + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
10 + */
11 public function user() 11 public function user()
12 { 12 {
13 - return $this->belongsTo('BookStack\User'); 13 + return $this->belongsTo(User::class);
14 } 14 }
15 +
15 } 16 }
......
1 <?php namespace BookStack; 1 <?php namespace BookStack;
2 2
3 3
4 -abstract class Entity extends Ownable 4 +class Entity extends Ownable
5 { 5 {
6 6
7 /** 7 /**
...@@ -43,7 +43,7 @@ abstract class Entity extends Ownable ...@@ -43,7 +43,7 @@ abstract class Entity extends Ownable
43 */ 43 */
44 public function activity() 44 public function activity()
45 { 45 {
46 - return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc'); 46 + return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
47 } 47 }
48 48
49 /** 49 /**
...@@ -51,15 +51,24 @@ abstract class Entity extends Ownable ...@@ -51,15 +51,24 @@ abstract class Entity extends Ownable
51 */ 51 */
52 public function views() 52 public function views()
53 { 53 {
54 - return $this->morphMany('BookStack\View', 'viewable'); 54 + return $this->morphMany(View::class, 'viewable');
55 + }
56 +
57 + /**
58 + * Get the Tag models that have been user assigned to this entity.
59 + * @return \Illuminate\Database\Eloquent\Relations\MorphMany
60 + */
61 + public function tags()
62 + {
63 + return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
55 } 64 }
56 65
57 /** 66 /**
58 * Get this entities restrictions. 67 * Get this entities restrictions.
59 */ 68 */
60 - public function restrictions() 69 + public function permissions()
61 { 70 {
62 - return $this->morphMany('BookStack\Restriction', 'restrictable'); 71 + return $this->morphMany(EntityPermission::class, 'restrictable');
63 } 72 }
64 73
65 /** 74 /**
...@@ -70,7 +79,28 @@ abstract class Entity extends Ownable ...@@ -70,7 +79,28 @@ abstract class Entity extends Ownable
70 */ 79 */
71 public function hasRestriction($role_id, $action) 80 public function hasRestriction($role_id, $action)
72 { 81 {
73 - return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0; 82 + return $this->permissions()->where('role_id', '=', $role_id)
83 + ->where('action', '=', $action)->count() > 0;
84 + }
85 +
86 + /**
87 + * Check if this entity has live (active) restrictions in place.
88 + * @param $role_id
89 + * @param $action
90 + * @return bool
91 + */
92 + public function hasActiveRestriction($role_id, $action)
93 + {
94 + return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
95 + }
96 +
97 + /**
98 + * Get the entity jointPermissions this is connected to.
99 + * @return \Illuminate\Database\Eloquent\Relations\MorphMany
100 + */
101 + public function jointPermissions()
102 + {
103 + return $this->morphMany(JointPermission::class, 'entity');
74 } 104 }
75 105
76 /** 106 /**
...@@ -81,7 +111,32 @@ abstract class Entity extends Ownable ...@@ -81,7 +111,32 @@ abstract class Entity extends Ownable
81 */ 111 */
82 public static function isA($type) 112 public static function isA($type)
83 { 113 {
84 - return static::getClassName() === strtolower($type); 114 + return static::getType() === strtolower($type);
115 + }
116 +
117 + /**
118 + * Get entity type.
119 + * @return mixed
120 + */
121 + public static function getType()
122 + {
123 + return strtolower(static::getClassName());
124 + }
125 +
126 + /**
127 + * Get an instance of an entity of the given type.
128 + * @param $type
129 + * @return Entity
130 + */
131 + public static function getEntityInstance($type)
132 + {
133 + $types = ['Page', 'Book', 'Chapter'];
134 + $className = str_replace([' ', '-', '_'], '', ucwords($type));
135 + if (!in_array($className, $types)) {
136 + return null;
137 + }
138 +
139 + return app('BookStack\\' . $className);
85 } 140 }
86 141
87 /** 142 /**
...@@ -102,54 +157,54 @@ abstract class Entity extends Ownable ...@@ -102,54 +157,54 @@ abstract class Entity extends Ownable
102 * @param string[] array $wheres 157 * @param string[] array $wheres
103 * @return mixed 158 * @return mixed
104 */ 159 */
105 - public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) 160 + public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
106 { 161 {
107 $exactTerms = []; 162 $exactTerms = [];
108 - foreach ($terms as $key => $term) { 163 + if (count($terms) === 0) {
109 - $term = htmlentities($term, ENT_QUOTES); 164 + $search = $this;
110 - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); 165 + $orderBy = 'updated_at';
111 - if (preg_match('/\s/', $term)) { 166 + } else {
112 - $exactTerms[] = '%' . $term . '%'; 167 + foreach ($terms as $key => $term) {
113 - $term = '"' . $term . '"'; 168 + $term = htmlentities($term, ENT_QUOTES);
114 - } else { 169 + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
115 - $term = '' . $term . '*'; 170 + if (preg_match('/\s/', $term)) {
171 + $exactTerms[] = '%' . $term . '%';
172 + $term = '"' . $term . '"';
173 + } else {
174 + $term = '' . $term . '*';
175 + }
176 + if ($term !== '*') $terms[$key] = $term;
116 } 177 }
117 - if ($term !== '*') $terms[$key] = $term; 178 + $termString = implode(' ', $terms);
118 - } 179 + $fields = implode(',', $fieldsToSearch);
119 - $termString = implode(' ', $terms); 180 + $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
120 - $fields = implode(',', $fieldsToSearch); 181 + $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
121 - $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); 182 +
122 - $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); 183 + // Ensure at least one exact term matches if in search
123 - 184 + if (count($exactTerms) > 0) {
124 - // Ensure at least one exact term matches if in search 185 + $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
125 - if (count($exactTerms) > 0) { 186 + foreach ($exactTerms as $exactTerm) {
126 - $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { 187 + foreach ($fieldsToSearch as $field) {
127 - foreach ($exactTerms as $exactTerm) { 188 + $query->orWhere($field, 'like', $exactTerm);
128 - foreach ($fieldsToSearch as $field) { 189 + }
129 - $query->orWhere($field, 'like', $exactTerm);
130 } 190 }
131 - } 191 + });
132 - }); 192 + }
133 - } 193 + $orderBy = 'title_relevance';
194 + };
134 195
135 // Add additional where terms 196 // Add additional where terms
136 foreach ($wheres as $whereTerm) { 197 foreach ($wheres as $whereTerm) {
137 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); 198 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
138 } 199 }
139 // Load in relations 200 // Load in relations
140 - if (static::isA('page')) { 201 + if ($this->isA('page')) {
141 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); 202 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
142 - } else if (static::isA('chapter')) { 203 + } else if ($this->isA('chapter')) {
143 $search = $search->with('book'); 204 $search = $search->with('book');
144 } 205 }
145 206
146 - return $search->orderBy('title_relevance', 'desc'); 207 + return $search->orderBy($orderBy, 'desc');
147 } 208 }
148 - 209 +
149 - /**
150 - * Get the url for this item.
151 - * @return string
152 - */
153 - abstract public function getUrl();
154 -
155 } 210 }
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 3
5 -use Illuminate\Database\Eloquent\Model; 4 +class EntityPermission extends Model
6 -
7 -class Restriction extends Model
8 { 5 {
9 6
10 protected $fillable = ['role_id', 'action']; 7 protected $fillable = ['role_id', 'action'];
...@@ -16,6 +13,6 @@ class Restriction extends Model ...@@ -16,6 +13,6 @@ class Restriction extends Model
16 */ 13 */
17 public function restrictable() 14 public function restrictable()
18 { 15 {
19 - return $this->morphTo(); 16 + return $this->morphTo('restrictable');
20 } 17 }
21 } 18 }
......
...@@ -71,11 +71,7 @@ class BookController extends Controller ...@@ -71,11 +71,7 @@ class BookController extends Controller
71 'name' => 'required|string|max:255', 71 'name' => 'required|string|max:255',
72 'description' => 'string|max:1000' 72 'description' => 'string|max:1000'
73 ]); 73 ]);
74 - $book = $this->bookRepo->newFromInput($request->all()); 74 + $book = $this->bookRepo->createFromInput($request->all());
75 - $book->slug = $this->bookRepo->findSuitableSlug($book->name);
76 - $book->created_by = Auth::user()->id;
77 - $book->updated_by = Auth::user()->id;
78 - $book->save();
79 Activity::add($book, 'book_create', $book->id); 75 Activity::add($book, 'book_create', $book->id);
80 return redirect($book->getUrl()); 76 return redirect($book->getUrl());
81 } 77 }
...@@ -88,6 +84,7 @@ class BookController extends Controller ...@@ -88,6 +84,7 @@ class BookController extends Controller
88 public function show($slug) 84 public function show($slug)
89 { 85 {
90 $book = $this->bookRepo->getBySlug($slug); 86 $book = $this->bookRepo->getBySlug($slug);
87 + $this->checkOwnablePermission('book-view', $book);
91 $bookChildren = $this->bookRepo->getChildren($book); 88 $bookChildren = $this->bookRepo->getChildren($book);
92 Views::add($book); 89 Views::add($book);
93 $this->setPageTitle($book->getShortName()); 90 $this->setPageTitle($book->getShortName());
...@@ -121,10 +118,7 @@ class BookController extends Controller ...@@ -121,10 +118,7 @@ class BookController extends Controller
121 'name' => 'required|string|max:255', 118 'name' => 'required|string|max:255',
122 'description' => 'string|max:1000' 119 'description' => 'string|max:1000'
123 ]); 120 ]);
124 - $book->fill($request->all()); 121 + $book = $this->bookRepo->updateFromInput($book, $request->all());
125 - $book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
126 - $book->updated_by = Auth::user()->id;
127 - $book->save();
128 Activity::add($book, 'book_update', $book->id); 122 Activity::add($book, 'book_update', $book->id);
129 return redirect($book->getUrl()); 123 return redirect($book->getUrl());
130 } 124 }
...@@ -209,6 +203,7 @@ class BookController extends Controller ...@@ -209,6 +203,7 @@ class BookController extends Controller
209 // Add activity for books 203 // Add activity for books
210 foreach ($sortedBooks as $bookId) { 204 foreach ($sortedBooks as $bookId) {
211 $updatedBook = $this->bookRepo->getById($bookId); 205 $updatedBook = $this->bookRepo->getById($bookId);
206 + $this->bookRepo->updateBookPermissions($updatedBook);
212 Activity::add($updatedBook, 'book_sort', $updatedBook->id); 207 Activity::add($updatedBook, 'book_sort', $updatedBook->id);
213 } 208 }
214 209
...@@ -226,7 +221,7 @@ class BookController extends Controller ...@@ -226,7 +221,7 @@ class BookController extends Controller
226 $this->checkOwnablePermission('book-delete', $book); 221 $this->checkOwnablePermission('book-delete', $book);
227 Activity::addMessage('book_delete', 0, $book->name); 222 Activity::addMessage('book_delete', 0, $book->name);
228 Activity::removeEntity($book); 223 Activity::removeEntity($book);
229 - $this->bookRepo->destroyBySlug($bookSlug); 224 + $this->bookRepo->destroy($book);
230 return redirect('/books'); 225 return redirect('/books');
231 } 226 }
232 227
...@@ -257,7 +252,7 @@ class BookController extends Controller ...@@ -257,7 +252,7 @@ class BookController extends Controller
257 { 252 {
258 $book = $this->bookRepo->getBySlug($bookSlug); 253 $book = $this->bookRepo->getBySlug($bookSlug);
259 $this->checkOwnablePermission('restrictions-manage', $book); 254 $this->checkOwnablePermission('restrictions-manage', $book);
260 - $this->bookRepo->updateRestrictionsFromRequest($request, $book); 255 + $this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
261 session()->flash('success', 'Book Restrictions Updated'); 256 session()->flash('success', 'Book Restrictions Updated');
262 return redirect($book->getUrl()); 257 return redirect($book->getUrl());
263 } 258 }
......
...@@ -57,12 +57,9 @@ class ChapterController extends Controller ...@@ -57,12 +57,9 @@ class ChapterController extends Controller
57 $book = $this->bookRepo->getBySlug($bookSlug); 57 $book = $this->bookRepo->getBySlug($bookSlug);
58 $this->checkOwnablePermission('chapter-create', $book); 58 $this->checkOwnablePermission('chapter-create', $book);
59 59
60 - $chapter = $this->chapterRepo->newFromInput($request->all()); 60 + $input = $request->all();
61 - $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id); 61 + $input['priority'] = $this->bookRepo->getNewPriority($book);
62 - $chapter->priority = $this->bookRepo->getNewPriority($book); 62 + $chapter = $this->chapterRepo->createFromInput($request->all(), $book);
63 - $chapter->created_by = auth()->user()->id;
64 - $chapter->updated_by = auth()->user()->id;
65 - $book->chapters()->save($chapter);
66 Activity::add($chapter, 'chapter_create', $book->id); 63 Activity::add($chapter, 'chapter_create', $book->id);
67 return redirect($chapter->getUrl()); 64 return redirect($chapter->getUrl());
68 } 65 }
...@@ -77,6 +74,7 @@ class ChapterController extends Controller ...@@ -77,6 +74,7 @@ class ChapterController extends Controller
77 { 74 {
78 $book = $this->bookRepo->getBySlug($bookSlug); 75 $book = $this->bookRepo->getBySlug($bookSlug);
79 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 76 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
77 + $this->checkOwnablePermission('chapter-view', $chapter);
80 $sidebarTree = $this->bookRepo->getChildren($book); 78 $sidebarTree = $this->bookRepo->getChildren($book);
81 Views::add($chapter); 79 Views::add($chapter);
82 $this->setPageTitle($chapter->getShortName()); 80 $this->setPageTitle($chapter->getShortName());
...@@ -186,7 +184,7 @@ class ChapterController extends Controller ...@@ -186,7 +184,7 @@ class ChapterController extends Controller
186 $book = $this->bookRepo->getBySlug($bookSlug); 184 $book = $this->bookRepo->getBySlug($bookSlug);
187 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); 185 $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
188 $this->checkOwnablePermission('restrictions-manage', $chapter); 186 $this->checkOwnablePermission('restrictions-manage', $chapter);
189 - $this->chapterRepo->updateRestrictionsFromRequest($request, $chapter); 187 + $this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
190 session()->flash('success', 'Chapter Restrictions Updated'); 188 session()->flash('success', 'Chapter Restrictions Updated');
191 return redirect($chapter->getUrl()); 189 return redirect($chapter->getUrl());
192 } 190 }
......
...@@ -110,4 +110,15 @@ abstract class Controller extends BaseController ...@@ -110,4 +110,15 @@ abstract class Controller extends BaseController
110 return true; 110 return true;
111 } 111 }
112 112
113 + /**
114 + * Send back a json error message.
115 + * @param string $messageText
116 + * @param int $statusCode
117 + * @return mixed
118 + */
119 + protected function jsonError($messageText = "", $statusCode = 500)
120 + {
121 + return response()->json(['message' => $messageText], $statusCode);
122 + }
123 +
113 } 124 }
......
...@@ -69,10 +69,10 @@ class PageController extends Controller ...@@ -69,10 +69,10 @@ class PageController extends Controller
69 { 69 {
70 $book = $this->bookRepo->getBySlug($bookSlug); 70 $book = $this->bookRepo->getBySlug($bookSlug);
71 $draft = $this->pageRepo->getById($pageId, true); 71 $draft = $this->pageRepo->getById($pageId, true);
72 - $this->checkOwnablePermission('page-create', $draft); 72 + $this->checkOwnablePermission('page-create', $book);
73 $this->setPageTitle('Edit Page Draft'); 73 $this->setPageTitle('Edit Page Draft');
74 74
75 - return view('pages/create', ['draft' => $draft, 'book' => $book]); 75 + return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
76 } 76 }
77 77
78 /** 78 /**
...@@ -128,6 +128,8 @@ class PageController extends Controller ...@@ -128,6 +128,8 @@ class PageController extends Controller
128 return redirect($page->getUrl()); 128 return redirect($page->getUrl());
129 } 129 }
130 130
131 + $this->checkOwnablePermission('page-view', $page);
132 +
131 $sidebarTree = $this->bookRepo->getChildren($book); 133 $sidebarTree = $this->bookRepo->getChildren($book);
132 Views::add($page); 134 Views::add($page);
133 $this->setPageTitle($page->getShortName()); 135 $this->setPageTitle($page->getShortName());
...@@ -449,7 +451,7 @@ class PageController extends Controller ...@@ -449,7 +451,7 @@ class PageController extends Controller
449 } 451 }
450 452
451 /** 453 /**
452 - * Set the restrictions for this page. 454 + * Set the permissions for this page.
453 * @param $bookSlug 455 * @param $bookSlug
454 * @param $pageSlug 456 * @param $pageSlug
455 * @param Request $request 457 * @param Request $request
...@@ -460,8 +462,8 @@ class PageController extends Controller ...@@ -460,8 +462,8 @@ class PageController extends Controller
460 $book = $this->bookRepo->getBySlug($bookSlug); 462 $book = $this->bookRepo->getBySlug($bookSlug);
461 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 463 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
462 $this->checkOwnablePermission('restrictions-manage', $page); 464 $this->checkOwnablePermission('restrictions-manage', $page);
463 - $this->pageRepo->updateRestrictionsFromRequest($request, $page); 465 + $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
464 - session()->flash('success', 'Page Restrictions Updated'); 466 + session()->flash('success', 'Page Permissions Updated');
465 return redirect($page->getUrl()); 467 return redirect($page->getUrl());
466 } 468 }
467 469
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 use BookStack\Exceptions\PermissionsException; 3 use BookStack\Exceptions\PermissionsException;
4 use BookStack\Repos\PermissionsRepo; 4 use BookStack\Repos\PermissionsRepo;
5 +use BookStack\Services\PermissionService;
5 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
6 use BookStack\Http\Requests; 7 use BookStack\Http\Requests;
7 8
...@@ -62,11 +63,13 @@ class PermissionController extends Controller ...@@ -62,11 +63,13 @@ class PermissionController extends Controller
62 * Show the form for editing a user role. 63 * Show the form for editing a user role.
63 * @param $id 64 * @param $id
64 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 65 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
66 + * @throws PermissionsException
65 */ 67 */
66 public function editRole($id) 68 public function editRole($id)
67 { 69 {
68 $this->checkPermission('user-roles-manage'); 70 $this->checkPermission('user-roles-manage');
69 $role = $this->permissionsRepo->getRoleById($id); 71 $role = $this->permissionsRepo->getRoleById($id);
72 + if ($role->hidden) throw new PermissionsException('This role cannot be edited');
70 return view('settings/roles/edit', ['role' => $role]); 73 return view('settings/roles/edit', ['role' => $role]);
71 } 74 }
72 75
......
1 +<?php namespace BookStack\Http\Controllers;
2 +
3 +use BookStack\Repos\TagRepo;
4 +use Illuminate\Http\Request;
5 +use BookStack\Http\Requests;
6 +
7 +class TagController extends Controller
8 +{
9 +
10 + protected $tagRepo;
11 +
12 + /**
13 + * TagController constructor.
14 + * @param $tagRepo
15 + */
16 + public function __construct(TagRepo $tagRepo)
17 + {
18 + $this->tagRepo = $tagRepo;
19 + }
20 +
21 + /**
22 + * Get all the Tags for a particular entity
23 + * @param $entityType
24 + * @param $entityId
25 + */
26 + public function getForEntity($entityType, $entityId)
27 + {
28 + $tags = $this->tagRepo->getForEntity($entityType, $entityId);
29 + return response()->json($tags);
30 + }
31 +
32 + /**
33 + * Update the tags for a particular entity.
34 + * @param $entityType
35 + * @param $entityId
36 + * @param Request $request
37 + * @return mixed
38 + */
39 + public function updateForEntity($entityType, $entityId, Request $request)
40 + {
41 + $entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
42 + if ($entity === null) return $this->jsonError("Entity not found", 404);
43 +
44 + $inputTags = $request->input('tags');
45 + $tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
46 + return response()->json([
47 + 'tags' => $tags,
48 + 'message' => 'Tags successfully updated'
49 + ]);
50 + }
51 +
52 + /**
53 + * Get tag name suggestions from a given search term.
54 + * @param Request $request
55 + */
56 + public function getNameSuggestions(Request $request)
57 + {
58 + $searchTerm = $request->get('search');
59 + $suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
60 + return response()->json($suggestions);
61 + }
62 +
63 + /**
64 + * Get tag value suggestions from a given search term.
65 + * @param Request $request
66 + */
67 + public function getValueSuggestions(Request $request)
68 + {
69 + $searchTerm = $request->get('search');
70 + $suggestions = $this->tagRepo->getValueSuggestions($searchTerm);
71 + return response()->json($suggestions);
72 + }
73 +
74 +}
...@@ -31,14 +31,21 @@ class UserController extends Controller ...@@ -31,14 +31,21 @@ class UserController extends Controller
31 31
32 /** 32 /**
33 * Display a listing of the users. 33 * Display a listing of the users.
34 + * @param Request $request
34 * @return Response 35 * @return Response
35 */ 36 */
36 - public function index() 37 + public function index(Request $request)
37 { 38 {
38 $this->checkPermission('users-manage'); 39 $this->checkPermission('users-manage');
39 - $users = $this->userRepo->getAllUsers(); 40 + $listDetails = [
41 + 'order' => $request->has('order') ? $request->get('order') : 'asc',
42 + 'search' => $request->has('search') ? $request->get('search') : '',
43 + 'sort' => $request->has('sort') ? $request->get('sort') : 'name',
44 + ];
45 + $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
40 $this->setPageTitle('Users'); 46 $this->setPageTitle('Users');
41 - return view('users/index', ['users' => $users]); 47 + $users->appends($listDetails);
48 + return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
42 } 49 }
43 50
44 /** 51 /**
...@@ -49,7 +56,8 @@ class UserController extends Controller ...@@ -49,7 +56,8 @@ class UserController extends Controller
49 { 56 {
50 $this->checkPermission('users-manage'); 57 $this->checkPermission('users-manage');
51 $authMethod = config('auth.method'); 58 $authMethod = config('auth.method');
52 - return view('users/create', ['authMethod' => $authMethod]); 59 + $roles = $this->userRepo->getAssignableRoles();
60 + return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
53 } 61 }
54 62
55 /** 63 /**
...@@ -117,7 +125,8 @@ class UserController extends Controller ...@@ -117,7 +125,8 @@ class UserController extends Controller
117 $user = $this->user->findOrFail($id); 125 $user = $this->user->findOrFail($id);
118 $activeSocialDrivers = $socialAuthService->getActiveDrivers(); 126 $activeSocialDrivers = $socialAuthService->getActiveDrivers();
119 $this->setPageTitle('User Profile'); 127 $this->setPageTitle('User Profile');
120 - return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]); 128 + $roles = $this->userRepo->getAssignableRoles();
129 + return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
121 } 130 }
122 131
123 /** 132 /**
...@@ -198,11 +207,14 @@ class UserController extends Controller ...@@ -198,11 +207,14 @@ class UserController extends Controller
198 }); 207 });
199 208
200 $user = $this->userRepo->getById($id); 209 $user = $this->userRepo->getById($id);
210 +
201 if ($this->userRepo->isOnlyAdmin($user)) { 211 if ($this->userRepo->isOnlyAdmin($user)) {
202 session()->flash('error', 'You cannot delete the only admin'); 212 session()->flash('error', 'You cannot delete the only admin');
203 return redirect($user->getEditUrl()); 213 return redirect($user->getEditUrl());
204 } 214 }
215 +
205 $this->userRepo->destroy($user); 216 $this->userRepo->destroy($user);
217 + session()->flash('success', 'User successfully removed');
206 218
207 return redirect('/settings/users'); 219 return redirect('/settings/users');
208 } 220 }
......
...@@ -28,7 +28,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -28,7 +28,7 @@ Route::group(['middleware' => 'auth'], function () {
28 // Pages 28 // Pages
29 Route::get('/{bookSlug}/page/create', 'PageController@create'); 29 Route::get('/{bookSlug}/page/create', 'PageController@create');
30 Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft'); 30 Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
31 - Route::post('/{bookSlug}/page/{pageId}', 'PageController@store'); 31 + Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
32 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 32 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
33 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf'); 33 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
34 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); 34 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
...@@ -80,11 +80,19 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -80,11 +80,19 @@ Route::group(['middleware' => 'auth'], function () {
80 Route::delete('/{imageId}', 'ImageController@destroy'); 80 Route::delete('/{imageId}', 'ImageController@destroy');
81 }); 81 });
82 82
83 - // Ajax routes 83 + // AJAX routes
84 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); 84 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
85 Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); 85 Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
86 Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy'); 86 Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
87 87
88 + // Tag routes (AJAX)
89 + Route::group(['prefix' => 'ajax/tags'], function() {
90 + Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
91 + Route::get('/suggest/names', 'TagController@getNameSuggestions');
92 + Route::get('/suggest/values', 'TagController@getValueSuggestions');
93 + Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
94 + });
95 +
88 // Links 96 // Links
89 Route::get('/link/{id}', 'PageController@redirectFromLink'); 97 Route::get('/link/{id}', 'PageController@redirectFromLink');
90 98
......
1 +<?php namespace BookStack;
2 +
3 +class JointPermission extends Model
4 +{
5 + public $timestamps = false;
6 +
7 + /**
8 + * Get the role that this points to.
9 + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
10 + */
11 + public function role()
12 + {
13 + return $this->belongsTo(Role::class);
14 + }
15 +
16 + /**
17 + * Get the entity this points to.
18 + * @return \Illuminate\Database\Eloquent\Relations\MorphOne
19 + */
20 + public function entity()
21 + {
22 + return $this->morphOne(Entity::class, 'entity');
23 + }
24 +}
1 +<?php namespace BookStack;
2 +
3 +use Illuminate\Database\Eloquent\Model as EloquentModel;
4 +
5 +class Model extends EloquentModel
6 +{
7 +
8 + /**
9 + * Provides public access to get the raw attribute value from the model.
10 + * Used in areas where no mutations are required but performance is critical.
11 + * @param $key
12 + * @return mixed
13 + */
14 + public function getRawAttribute($key)
15 + {
16 + return parent::getAttributeFromArray($key);
17 + }
18 +
19 +}
...\ No newline at end of file ...\ No newline at end of file
1 <?php namespace BookStack; 1 <?php namespace BookStack;
2 2
3 -use Illuminate\Database\Eloquent\Model;
4 3
5 abstract class Ownable extends Model 4 abstract class Ownable extends Model
6 { 5 {
...@@ -10,7 +9,7 @@ abstract class Ownable extends Model ...@@ -10,7 +9,7 @@ abstract class Ownable extends Model
10 */ 9 */
11 public function createdBy() 10 public function createdBy()
12 { 11 {
13 - return $this->belongsTo('BookStack\User', 'created_by'); 12 + return $this->belongsTo(User::class, 'created_by');
14 } 13 }
15 14
16 /** 15 /**
...@@ -19,7 +18,7 @@ abstract class Ownable extends Model ...@@ -19,7 +18,7 @@ abstract class Ownable extends Model
19 */ 18 */
20 public function updatedBy() 19 public function updatedBy()
21 { 20 {
22 - return $this->belongsTo('BookStack\User', 'updated_by'); 21 + return $this->belongsTo(User::class, 'updated_by');
23 } 22 }
24 23
25 /** 24 /**
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 -
5 -use Illuminate\Database\Eloquent\Model;
6 3
7 class Page extends Entity 4 class Page extends Entity
8 { 5 {
...@@ -10,6 +7,10 @@ class Page extends Entity ...@@ -10,6 +7,10 @@ class Page extends Entity
10 7
11 protected $simpleAttributes = ['name', 'id', 'slug']; 8 protected $simpleAttributes = ['name', 'id', 'slug'];
12 9
10 + /**
11 + * Converts this page into a simplified array.
12 + * @return mixed
13 + */
13 public function toSimpleArray() 14 public function toSimpleArray()
14 { 15 {
15 $array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes)); 16 $array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
...@@ -17,26 +18,46 @@ class Page extends Entity ...@@ -17,26 +18,46 @@ class Page extends Entity
17 return $array; 18 return $array;
18 } 19 }
19 20
21 + /**
22 + * Get the book this page sits in.
23 + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
24 + */
20 public function book() 25 public function book()
21 { 26 {
22 - return $this->belongsTo('BookStack\Book'); 27 + return $this->belongsTo(Book::class);
23 } 28 }
24 29
30 + /**
31 + * Get the chapter that this page is in, If applicable.
32 + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
33 + */
25 public function chapter() 34 public function chapter()
26 { 35 {
27 - return $this->belongsTo('BookStack\Chapter'); 36 + return $this->belongsTo(Chapter::class);
28 } 37 }
29 38
39 + /**
40 + * Check if this page has a chapter.
41 + * @return bool
42 + */
30 public function hasChapter() 43 public function hasChapter()
31 { 44 {
32 return $this->chapter()->count() > 0; 45 return $this->chapter()->count() > 0;
33 } 46 }
34 47
48 + /**
49 + * Get the associated page revisions, ordered by created date.
50 + * @return mixed
51 + */
35 public function revisions() 52 public function revisions()
36 { 53 {
37 - return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc'); 54 + return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
38 } 55 }
39 56
57 + /**
58 + * Get the url for this page.
59 + * @return string
60 + */
40 public function getUrl() 61 public function getUrl()
41 { 62 {
42 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; 63 $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
...@@ -45,6 +66,11 @@ class Page extends Entity ...@@ -45,6 +66,11 @@ class Page extends Entity
45 return '/books/' . $bookSlug . $midText . $idComponent; 66 return '/books/' . $bookSlug . $midText . $idComponent;
46 } 67 }
47 68
69 + /**
70 + * Get an excerpt of this page's content to the specified length.
71 + * @param int $length
72 + * @return mixed
73 + */
48 public function getExcerpt($length = 100) 74 public function getExcerpt($length = 100)
49 { 75 {
50 $text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text; 76 $text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
......
1 <?php namespace BookStack; 1 <?php namespace BookStack;
2 2
3 -use Illuminate\Database\Eloquent\Model;
4 3
5 class PageRevision extends Model 4 class PageRevision extends Model
6 { 5 {
...@@ -12,7 +11,7 @@ class PageRevision extends Model ...@@ -12,7 +11,7 @@ class PageRevision extends Model
12 */ 11 */
13 public function createdBy() 12 public function createdBy()
14 { 13 {
15 - return $this->belongsTo('BookStack\User', 'created_by'); 14 + return $this->belongsTo(User::class, 'created_by');
16 } 15 }
17 16
18 /** 17 /**
...@@ -21,7 +20,7 @@ class PageRevision extends Model ...@@ -21,7 +20,7 @@ class PageRevision extends Model
21 */ 20 */
22 public function page() 21 public function page()
23 { 22 {
24 - return $this->belongsTo('BookStack\Page'); 23 + return $this->belongsTo(Page::class);
25 } 24 }
26 25
27 /** 26 /**
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
3 namespace BookStack\Providers; 3 namespace BookStack\Providers;
4 4
5 use Auth; 5 use Auth;
6 +use BookStack\Services\LdapService;
6 use Illuminate\Support\ServiceProvider; 7 use Illuminate\Support\ServiceProvider;
7 8
8 class AuthServiceProvider extends ServiceProvider 9 class AuthServiceProvider extends ServiceProvider
...@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider ...@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
25 public function register() 26 public function register()
26 { 27 {
27 Auth::provider('ldap', function($app, array $config) { 28 Auth::provider('ldap', function($app, array $config) {
28 - return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']); 29 + return new LdapUserProvider($config['model'], $app[LdapService::class]);
29 }); 30 });
30 } 31 }
31 } 32 }
......
...@@ -2,11 +2,18 @@ ...@@ -2,11 +2,18 @@
2 2
3 namespace BookStack\Providers; 3 namespace BookStack\Providers;
4 4
5 +use BookStack\Activity;
5 use BookStack\Services\ImageService; 6 use BookStack\Services\ImageService;
7 +use BookStack\Services\PermissionService;
6 use BookStack\Services\ViewService; 8 use BookStack\Services\ViewService;
9 +use BookStack\Setting;
10 +use BookStack\View;
11 +use Illuminate\Contracts\Cache\Repository;
12 +use Illuminate\Contracts\Filesystem\Factory;
7 use Illuminate\Support\ServiceProvider; 13 use Illuminate\Support\ServiceProvider;
8 use BookStack\Services\ActivityService; 14 use BookStack\Services\ActivityService;
9 use BookStack\Services\SettingService; 15 use BookStack\Services\SettingService;
16 +use Intervention\Image\ImageManager;
10 17
11 class CustomFacadeProvider extends ServiceProvider 18 class CustomFacadeProvider extends ServiceProvider
12 { 19 {
...@@ -29,30 +36,30 @@ class CustomFacadeProvider extends ServiceProvider ...@@ -29,30 +36,30 @@ class CustomFacadeProvider extends ServiceProvider
29 { 36 {
30 $this->app->bind('activity', function() { 37 $this->app->bind('activity', function() {
31 return new ActivityService( 38 return new ActivityService(
32 - $this->app->make('BookStack\Activity'), 39 + $this->app->make(Activity::class),
33 - $this->app->make('BookStack\Services\RestrictionService') 40 + $this->app->make(PermissionService::class)
34 ); 41 );
35 }); 42 });
36 43
37 $this->app->bind('views', function() { 44 $this->app->bind('views', function() {
38 return new ViewService( 45 return new ViewService(
39 - $this->app->make('BookStack\View'), 46 + $this->app->make(View::class),
40 - $this->app->make('BookStack\Services\RestrictionService') 47 + $this->app->make(PermissionService::class)
41 ); 48 );
42 }); 49 });
43 50
44 $this->app->bind('setting', function() { 51 $this->app->bind('setting', function() {
45 return new SettingService( 52 return new SettingService(
46 - $this->app->make('BookStack\Setting'), 53 + $this->app->make(Setting::class),
47 - $this->app->make('Illuminate\Contracts\Cache\Repository') 54 + $this->app->make(Repository::class)
48 ); 55 );
49 }); 56 });
50 57
51 $this->app->bind('images', function() { 58 $this->app->bind('images', function() {
52 return new ImageService( 59 return new ImageService(
53 - $this->app->make('Intervention\Image\ImageManager'), 60 + $this->app->make(ImageManager::class),
54 - $this->app->make('Illuminate\Contracts\Filesystem\Factory'), 61 + $this->app->make(Factory::class),
55 - $this->app->make('Illuminate\Contracts\Cache\Repository') 62 + $this->app->make(Repository::class)
56 ); 63 );
57 }); 64 });
58 } 65 }
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 +use Alpha\B;
3 use BookStack\Exceptions\NotFoundException; 4 use BookStack\Exceptions\NotFoundException;
4 use Illuminate\Support\Str; 5 use Illuminate\Support\Str;
5 use BookStack\Book; 6 use BookStack\Book;
...@@ -29,7 +30,7 @@ class BookRepo extends EntityRepo ...@@ -29,7 +30,7 @@ class BookRepo extends EntityRepo
29 */ 30 */
30 private function bookQuery() 31 private function bookQuery()
31 { 32 {
32 - return $this->restrictionService->enforceBookRestrictions($this->book, 'view'); 33 + return $this->permissionService->enforceBookRestrictions($this->book, 'view');
33 } 34 }
34 35
35 /** 36 /**
...@@ -123,21 +124,43 @@ class BookRepo extends EntityRepo ...@@ -123,21 +124,43 @@ class BookRepo extends EntityRepo
123 124
124 /** 125 /**
125 * Get a new book instance from request input. 126 * Get a new book instance from request input.
127 + * @param array $input
128 + * @return Book
129 + */
130 + public function createFromInput($input)
131 + {
132 + $book = $this->book->newInstance($input);
133 + $book->slug = $this->findSuitableSlug($book->name);
134 + $book->created_by = auth()->user()->id;
135 + $book->updated_by = auth()->user()->id;
136 + $book->save();
137 + $this->permissionService->buildJointPermissionsForEntity($book);
138 + return $book;
139 + }
140 +
141 + /**
142 + * Update the given book from user input.
143 + * @param Book $book
126 * @param $input 144 * @param $input
127 * @return Book 145 * @return Book
128 */ 146 */
129 - public function newFromInput($input) 147 + public function updateFromInput(Book $book, $input)
130 { 148 {
131 - return $this->book->newInstance($input); 149 + $book->fill($input);
150 + $book->slug = $this->findSuitableSlug($book->name, $book->id);
151 + $book->updated_by = auth()->user()->id;
152 + $book->save();
153 + $this->permissionService->buildJointPermissionsForEntity($book);
154 + return $book;
132 } 155 }
133 156
134 /** 157 /**
135 - * Destroy a book identified by the given slug. 158 + * Destroy the given book.
136 - * @param $bookSlug 159 + * @param Book $book
160 + * @throws \Exception
137 */ 161 */
138 - public function destroyBySlug($bookSlug) 162 + public function destroy(Book $book)
139 { 163 {
140 - $book = $this->getBySlug($bookSlug);
141 foreach ($book->pages as $page) { 164 foreach ($book->pages as $page) {
142 $this->pageRepo->destroy($page); 165 $this->pageRepo->destroy($page);
143 } 166 }
...@@ -145,11 +168,21 @@ class BookRepo extends EntityRepo ...@@ -145,11 +168,21 @@ class BookRepo extends EntityRepo
145 $this->chapterRepo->destroy($chapter); 168 $this->chapterRepo->destroy($chapter);
146 } 169 }
147 $book->views()->delete(); 170 $book->views()->delete();
148 - $book->restrictions()->delete(); 171 + $book->permissions()->delete();
172 + $this->permissionService->deleteJointPermissionsForEntity($book);
149 $book->delete(); 173 $book->delete();
150 } 174 }
151 175
152 /** 176 /**
177 + * Alias method to update the book jointPermissions in the PermissionService.
178 + * @param Book $book
179 + */
180 + public function updateBookPermissions(Book $book)
181 + {
182 + $this->permissionService->buildJointPermissionsForEntity($book);
183 + }
184 +
185 + /**
153 * Get the next child element priority. 186 * Get the next child element priority.
154 * @param Book $book 187 * @param Book $book
155 * @return int 188 * @return int
...@@ -204,7 +237,7 @@ class BookRepo extends EntityRepo ...@@ -204,7 +237,7 @@ class BookRepo extends EntityRepo
204 public function getChildren(Book $book, $filterDrafts = false) 237 public function getChildren(Book $book, $filterDrafts = false)
205 { 238 {
206 $pageQuery = $book->pages()->where('chapter_id', '=', 0); 239 $pageQuery = $book->pages()->where('chapter_id', '=', 0);
207 - $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view'); 240 + $pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
208 241
209 if ($filterDrafts) { 242 if ($filterDrafts) {
210 $pageQuery = $pageQuery->where('draft', '=', false); 243 $pageQuery = $pageQuery->where('draft', '=', false);
...@@ -213,10 +246,10 @@ class BookRepo extends EntityRepo ...@@ -213,10 +246,10 @@ class BookRepo extends EntityRepo
213 $pages = $pageQuery->get(); 246 $pages = $pageQuery->get();
214 247
215 $chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) { 248 $chapterQuery = $book->chapters()->with(['pages' => function($query) use ($filterDrafts) {
216 - $this->restrictionService->enforcePageRestrictions($query, 'view'); 249 + $this->permissionService->enforcePageRestrictions($query, 'view');
217 if ($filterDrafts) $query->where('draft', '=', false); 250 if ($filterDrafts) $query->where('draft', '=', false);
218 }]); 251 }]);
219 - $chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view'); 252 + $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
220 $chapters = $chapterQuery->get(); 253 $chapters = $chapterQuery->get();
221 $children = $pages->merge($chapters); 254 $children = $pages->merge($chapters);
222 $bookSlug = $book->slug; 255 $bookSlug = $book->slug;
...@@ -253,8 +286,9 @@ class BookRepo extends EntityRepo ...@@ -253,8 +286,9 @@ class BookRepo extends EntityRepo
253 public function getBySearch($term, $count = 20, $paginationAppends = []) 286 public function getBySearch($term, $count = 20, $paginationAppends = [])
254 { 287 {
255 $terms = $this->prepareSearchTerms($term); 288 $terms = $this->prepareSearchTerms($term);
256 - $books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) 289 + $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
257 - ->paginate($count)->appends($paginationAppends); 290 + $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
291 + $books = $bookQuery->paginate($count)->appends($paginationAppends);
258 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 292 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
259 foreach ($books as $book) { 293 foreach ($books as $book) {
260 //highlight 294 //highlight
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 3
4 use Activity; 4 use Activity;
5 +use BookStack\Book;
5 use BookStack\Exceptions\NotFoundException; 6 use BookStack\Exceptions\NotFoundException;
6 use Illuminate\Support\Str; 7 use Illuminate\Support\Str;
7 use BookStack\Chapter; 8 use BookStack\Chapter;
...@@ -9,12 +10,12 @@ use BookStack\Chapter; ...@@ -9,12 +10,12 @@ use BookStack\Chapter;
9 class ChapterRepo extends EntityRepo 10 class ChapterRepo extends EntityRepo
10 { 11 {
11 /** 12 /**
12 - * Base query for getting chapters, Takes restrictions into account. 13 + * Base query for getting chapters, Takes permissions into account.
13 * @return mixed 14 * @return mixed
14 */ 15 */
15 private function chapterQuery() 16 private function chapterQuery()
16 { 17 {
17 - return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view'); 18 + return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
18 } 19 }
19 20
20 /** 21 /**
...@@ -66,7 +67,7 @@ class ChapterRepo extends EntityRepo ...@@ -66,7 +67,7 @@ class ChapterRepo extends EntityRepo
66 */ 67 */
67 public function getChildren(Chapter $chapter) 68 public function getChildren(Chapter $chapter)
68 { 69 {
69 - $pages = $this->restrictionService->enforcePageRestrictions($chapter->pages())->get(); 70 + $pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
70 // Sort items with drafts first then by priority. 71 // Sort items with drafts first then by priority.
71 return $pages->sortBy(function($child, $key) { 72 return $pages->sortBy(function($child, $key) {
72 $score = $child->priority; 73 $score = $child->priority;
...@@ -78,11 +79,18 @@ class ChapterRepo extends EntityRepo ...@@ -78,11 +79,18 @@ class ChapterRepo extends EntityRepo
78 /** 79 /**
79 * Create a new chapter from request input. 80 * Create a new chapter from request input.
80 * @param $input 81 * @param $input
81 - * @return $this 82 + * @param Book $book
83 + * @return Chapter
82 */ 84 */
83 - public function newFromInput($input) 85 + public function createFromInput($input, Book $book)
84 { 86 {
85 - return $this->chapter->fill($input); 87 + $chapter = $this->chapter->newInstance($input);
88 + $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
89 + $chapter->created_by = auth()->user()->id;
90 + $chapter->updated_by = auth()->user()->id;
91 + $chapter = $book->chapters()->save($chapter);
92 + $this->permissionService->buildJointPermissionsForEntity($chapter);
93 + return $chapter;
86 } 94 }
87 95
88 /** 96 /**
...@@ -99,7 +107,8 @@ class ChapterRepo extends EntityRepo ...@@ -99,7 +107,8 @@ class ChapterRepo extends EntityRepo
99 } 107 }
100 Activity::removeEntity($chapter); 108 Activity::removeEntity($chapter);
101 $chapter->views()->delete(); 109 $chapter->views()->delete();
102 - $chapter->restrictions()->delete(); 110 + $chapter->permissions()->delete();
111 + $this->permissionService->deleteJointPermissionsForEntity($chapter);
103 $chapter->delete(); 112 $chapter->delete();
104 } 113 }
105 114
...@@ -159,8 +168,9 @@ class ChapterRepo extends EntityRepo ...@@ -159,8 +168,9 @@ class ChapterRepo extends EntityRepo
159 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) 168 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
160 { 169 {
161 $terms = $this->prepareSearchTerms($term); 170 $terms = $this->prepareSearchTerms($term);
162 - $chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) 171 + $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
163 - ->paginate($count)->appends($paginationAppends); 172 + $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
173 + $chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
164 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 174 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
165 foreach ($chapters as $chapter) { 175 foreach ($chapters as $chapter) {
166 //highlight 176 //highlight
......
...@@ -4,8 +4,9 @@ use BookStack\Book; ...@@ -4,8 +4,9 @@ use BookStack\Book;
4 use BookStack\Chapter; 4 use BookStack\Chapter;
5 use BookStack\Entity; 5 use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 -use BookStack\Services\RestrictionService; 7 +use BookStack\Services\PermissionService;
8 use BookStack\User; 8 use BookStack\User;
9 +use Illuminate\Support\Facades\Log;
9 10
10 class EntityRepo 11 class EntityRepo
11 { 12 {
...@@ -26,9 +27,15 @@ class EntityRepo ...@@ -26,9 +27,15 @@ class EntityRepo
26 public $page; 27 public $page;
27 28
28 /** 29 /**
29 - * @var RestrictionService 30 + * @var PermissionService
30 */ 31 */
31 - protected $restrictionService; 32 + protected $permissionService;
33 +
34 + /**
35 + * Acceptable operators to be used in a query
36 + * @var array
37 + */
38 + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
32 39
33 /** 40 /**
34 * EntityService constructor. 41 * EntityService constructor.
...@@ -38,7 +45,7 @@ class EntityRepo ...@@ -38,7 +45,7 @@ class EntityRepo
38 $this->book = app(Book::class); 45 $this->book = app(Book::class);
39 $this->chapter = app(Chapter::class); 46 $this->chapter = app(Chapter::class);
40 $this->page = app(Page::class); 47 $this->page = app(Page::class);
41 - $this->restrictionService = app(RestrictionService::class); 48 + $this->permissionService = app(PermissionService::class);
42 } 49 }
43 50
44 /** 51 /**
...@@ -50,7 +57,7 @@ class EntityRepo ...@@ -50,7 +57,7 @@ class EntityRepo
50 */ 57 */
51 public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false) 58 public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
52 { 59 {
53 - $query = $this->restrictionService->enforceBookRestrictions($this->book) 60 + $query = $this->permissionService->enforceBookRestrictions($this->book)
54 ->orderBy('created_at', 'desc'); 61 ->orderBy('created_at', 'desc');
55 if ($additionalQuery !== false && is_callable($additionalQuery)) { 62 if ($additionalQuery !== false && is_callable($additionalQuery)) {
56 $additionalQuery($query); 63 $additionalQuery($query);
...@@ -66,7 +73,7 @@ class EntityRepo ...@@ -66,7 +73,7 @@ class EntityRepo
66 */ 73 */
67 public function getRecentlyUpdatedBooks($count = 20, $page = 0) 74 public function getRecentlyUpdatedBooks($count = 20, $page = 0)
68 { 75 {
69 - return $this->restrictionService->enforceBookRestrictions($this->book) 76 + return $this->permissionService->enforceBookRestrictions($this->book)
70 ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get(); 77 ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
71 } 78 }
72 79
...@@ -79,7 +86,7 @@ class EntityRepo ...@@ -79,7 +86,7 @@ class EntityRepo
79 */ 86 */
80 public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false) 87 public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
81 { 88 {
82 - $query = $this->restrictionService->enforcePageRestrictions($this->page) 89 + $query = $this->permissionService->enforcePageRestrictions($this->page)
83 ->orderBy('created_at', 'desc')->where('draft', '=', false); 90 ->orderBy('created_at', 'desc')->where('draft', '=', false);
84 if ($additionalQuery !== false && is_callable($additionalQuery)) { 91 if ($additionalQuery !== false && is_callable($additionalQuery)) {
85 $additionalQuery($query); 92 $additionalQuery($query);
...@@ -96,7 +103,7 @@ class EntityRepo ...@@ -96,7 +103,7 @@ class EntityRepo
96 */ 103 */
97 public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false) 104 public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
98 { 105 {
99 - $query = $this->restrictionService->enforceChapterRestrictions($this->chapter) 106 + $query = $this->permissionService->enforceChapterRestrictions($this->chapter)
100 ->orderBy('created_at', 'desc'); 107 ->orderBy('created_at', 'desc');
101 if ($additionalQuery !== false && is_callable($additionalQuery)) { 108 if ($additionalQuery !== false && is_callable($additionalQuery)) {
102 $additionalQuery($query); 109 $additionalQuery($query);
...@@ -112,7 +119,7 @@ class EntityRepo ...@@ -112,7 +119,7 @@ class EntityRepo
112 */ 119 */
113 public function getRecentlyUpdatedPages($count = 20, $page = 0) 120 public function getRecentlyUpdatedPages($count = 20, $page = 0)
114 { 121 {
115 - return $this->restrictionService->enforcePageRestrictions($this->page) 122 + return $this->permissionService->enforcePageRestrictions($this->page)
116 ->where('draft', '=', false) 123 ->where('draft', '=', false)
117 ->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get(); 124 ->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
118 } 125 }
...@@ -136,14 +143,14 @@ class EntityRepo ...@@ -136,14 +143,14 @@ class EntityRepo
136 * @param $request 143 * @param $request
137 * @param Entity $entity 144 * @param Entity $entity
138 */ 145 */
139 - public function updateRestrictionsFromRequest($request, Entity $entity) 146 + public function updateEntityPermissionsFromRequest($request, Entity $entity)
140 { 147 {
141 $entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; 148 $entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
142 - $entity->restrictions()->delete(); 149 + $entity->permissions()->delete();
143 if ($request->has('restrictions')) { 150 if ($request->has('restrictions')) {
144 foreach ($request->get('restrictions') as $roleId => $restrictions) { 151 foreach ($request->get('restrictions') as $roleId => $restrictions) {
145 foreach ($restrictions as $action => $value) { 152 foreach ($restrictions as $action => $value) {
146 - $entity->restrictions()->create([ 153 + $entity->permissions()->create([
147 'role_id' => $roleId, 154 'role_id' => $roleId,
148 'action' => strtolower($action) 155 'action' => strtolower($action)
149 ]); 156 ]);
...@@ -151,6 +158,7 @@ class EntityRepo ...@@ -151,6 +158,7 @@ class EntityRepo
151 } 158 }
152 } 159 }
153 $entity->save(); 160 $entity->save();
161 + $this->permissionService->buildJointPermissionsForEntity($entity);
154 } 162 }
155 163
156 /** 164 /**
...@@ -162,6 +170,7 @@ class EntityRepo ...@@ -162,6 +170,7 @@ class EntityRepo
162 */ 170 */
163 protected function prepareSearchTerms($termString) 171 protected function prepareSearchTerms($termString)
164 { 172 {
173 + $termString = $this->cleanSearchTermString($termString);
165 preg_match_all('/"(.*?)"/', $termString, $matches); 174 preg_match_all('/"(.*?)"/', $termString, $matches);
166 if (count($matches[1]) > 0) { 175 if (count($matches[1]) > 0) {
167 $terms = $matches[1]; 176 $terms = $matches[1];
...@@ -173,5 +182,93 @@ class EntityRepo ...@@ -173,5 +182,93 @@ class EntityRepo
173 return $terms; 182 return $terms;
174 } 183 }
175 184
185 + /**
186 + * Removes any special search notation that should not
187 + * be used in a full-text search.
188 + * @param $termString
189 + * @return mixed
190 + */
191 + protected function cleanSearchTermString($termString)
192 + {
193 + // Strip tag searches
194 + $termString = preg_replace('/\[.*?\]/', '', $termString);
195 + // Reduced multiple spacing into single spacing
196 + $termString = preg_replace("/\s{2,}/", " ", $termString);
197 + return $termString;
198 + }
199 +
200 + /**
201 + * Get the available query operators as a regex escaped list.
202 + * @return mixed
203 + */
204 + protected function getRegexEscapedOperators()
205 + {
206 + $escapedOperators = [];
207 + foreach ($this->queryOperators as $operator) {
208 + $escapedOperators[] = preg_quote($operator);
209 + }
210 + return join('|', $escapedOperators);
211 + }
212 +
213 + /**
214 + * Parses advanced search notations and adds them to the db query.
215 + * @param $query
216 + * @param $termString
217 + * @return mixed
218 + */
219 + protected function addAdvancedSearchQueries($query, $termString)
220 + {
221 + $escapedOperators = $this->getRegexEscapedOperators();
222 + // Look for tag searches
223 + preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
224 + if (count($tags[0]) > 0) {
225 + $this->applyTagSearches($query, $tags);
226 + }
227 +
228 + return $query;
229 + }
230 +
231 + /**
232 + * Apply extracted tag search terms onto a entity query.
233 + * @param $query
234 + * @param $tags
235 + * @return mixed
236 + */
237 + protected function applyTagSearches($query, $tags) {
238 + $query->where(function($query) use ($tags) {
239 + foreach ($tags[1] as $index => $tagName) {
240 + $query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
241 + $tagOperator = $tags[3][$index];
242 + $tagValue = $tags[4][$index];
243 + if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
244 + if (is_numeric($tagValue) && $tagOperator !== 'like') {
245 + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
246 + // search the value as a string which prevents being able to do number-based operations
247 + // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
248 + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
249 + $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
250 + } else {
251 + $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
252 + }
253 + } else {
254 + $query->where('name', '=', $tagName);
255 + }
256 + });
257 + }
258 + });
259 + return $query;
260 + }
261 +
262 +}
263 +
264 +
265 +
266 +
267 +
268 +
269 +
270 +
271 +
272 +
273 +
176 274
177 -}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 use BookStack\Image; 4 use BookStack\Image;
5 use BookStack\Page; 5 use BookStack\Page;
6 use BookStack\Services\ImageService; 6 use BookStack\Services\ImageService;
7 -use BookStack\Services\RestrictionService; 7 +use BookStack\Services\PermissionService;
8 use Setting; 8 use Setting;
9 use Symfony\Component\HttpFoundation\File\UploadedFile; 9 use Symfony\Component\HttpFoundation\File\UploadedFile;
10 10
...@@ -20,14 +20,14 @@ class ImageRepo ...@@ -20,14 +20,14 @@ class ImageRepo
20 * ImageRepo constructor. 20 * ImageRepo constructor.
21 * @param Image $image 21 * @param Image $image
22 * @param ImageService $imageService 22 * @param ImageService $imageService
23 - * @param RestrictionService $restrictionService 23 + * @param PermissionService $permissionService
24 * @param Page $page 24 * @param Page $page
25 */ 25 */
26 - public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page) 26 + public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
27 { 27 {
28 $this->image = $image; 28 $this->image = $image;
29 $this->imageService = $imageService; 29 $this->imageService = $imageService;
30 - $this->restictionService = $restrictionService; 30 + $this->restictionService = $permissionService;
31 $this->page = $page; 31 $this->page = $page;
32 } 32 }
33 33
......
...@@ -14,14 +14,17 @@ class PageRepo extends EntityRepo ...@@ -14,14 +14,17 @@ class PageRepo extends EntityRepo
14 { 14 {
15 15
16 protected $pageRevision; 16 protected $pageRevision;
17 + protected $tagRepo;
17 18
18 /** 19 /**
19 * PageRepo constructor. 20 * PageRepo constructor.
20 * @param PageRevision $pageRevision 21 * @param PageRevision $pageRevision
22 + * @param TagRepo $tagRepo
21 */ 23 */
22 - public function __construct(PageRevision $pageRevision) 24 + public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
23 { 25 {
24 $this->pageRevision = $pageRevision; 26 $this->pageRevision = $pageRevision;
27 + $this->tagRepo = $tagRepo;
25 parent::__construct(); 28 parent::__construct();
26 } 29 }
27 30
...@@ -32,7 +35,7 @@ class PageRepo extends EntityRepo ...@@ -32,7 +35,7 @@ class PageRepo extends EntityRepo
32 */ 35 */
33 private function pageQuery($allowDrafts = false) 36 private function pageQuery($allowDrafts = false)
34 { 37 {
35 - $query = $this->restrictionService->enforcePageRestrictions($this->page, 'view'); 38 + $query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
36 if (!$allowDrafts) { 39 if (!$allowDrafts) {
37 $query = $query->where('draft', '=', false); 40 $query = $query->where('draft', '=', false);
38 } 41 }
...@@ -76,7 +79,7 @@ class PageRepo extends EntityRepo ...@@ -76,7 +79,7 @@ class PageRepo extends EntityRepo
76 { 79 {
77 $revision = $this->pageRevision->where('slug', '=', $pageSlug) 80 $revision = $this->pageRevision->where('slug', '=', $pageSlug)
78 ->whereHas('page', function ($query) { 81 ->whereHas('page', function ($query) {
79 - $this->restrictionService->enforcePageRestrictions($query); 82 + $this->permissionService->enforcePageRestrictions($query);
80 }) 83 })
81 ->where('type', '=', 'version') 84 ->where('type', '=', 'version')
82 ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') 85 ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
...@@ -142,6 +145,11 @@ class PageRepo extends EntityRepo ...@@ -142,6 +145,11 @@ class PageRepo extends EntityRepo
142 { 145 {
143 $draftPage->fill($input); 146 $draftPage->fill($input);
144 147
148 + // Save page tags if present
149 + if(isset($input['tags'])) {
150 + $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
151 + }
152 +
145 $draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id); 153 $draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
146 $draftPage->html = $this->formatHtml($input['html']); 154 $draftPage->html = $this->formatHtml($input['html']);
147 $draftPage->text = strip_tags($draftPage->html); 155 $draftPage->text = strip_tags($draftPage->html);
...@@ -168,6 +176,7 @@ class PageRepo extends EntityRepo ...@@ -168,6 +176,7 @@ class PageRepo extends EntityRepo
168 if ($chapter) $page->chapter_id = $chapter->id; 176 if ($chapter) $page->chapter_id = $chapter->id;
169 177
170 $book->pages()->save($page); 178 $book->pages()->save($page);
179 + $this->permissionService->buildJointPermissionsForEntity($page);
171 return $page; 180 return $page;
172 } 181 }
173 182
...@@ -241,8 +250,9 @@ class PageRepo extends EntityRepo ...@@ -241,8 +250,9 @@ class PageRepo extends EntityRepo
241 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) 250 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
242 { 251 {
243 $terms = $this->prepareSearchTerms($term); 252 $terms = $this->prepareSearchTerms($term);
244 - $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) 253 + $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
245 - ->paginate($count)->appends($paginationAppends); 254 + $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
255 + $pages = $pageQuery->paginate($count)->appends($paginationAppends);
246 256
247 // Add highlights to page text. 257 // Add highlights to page text.
248 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 258 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
...@@ -307,6 +317,11 @@ class PageRepo extends EntityRepo ...@@ -307,6 +317,11 @@ class PageRepo extends EntityRepo
307 $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id); 317 $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
308 } 318 }
309 319
320 + // Save page tags if present
321 + if(isset($input['tags'])) {
322 + $this->tagRepo->saveTagsToEntity($page, $input['tags']);
323 + }
324 +
310 // Update with new details 325 // Update with new details
311 $userId = auth()->user()->id; 326 $userId = auth()->user()->id;
312 $page->fill($input); 327 $page->fill($input);
...@@ -577,12 +592,14 @@ class PageRepo extends EntityRepo ...@@ -577,12 +592,14 @@ class PageRepo extends EntityRepo
577 * Destroy a given page along with its dependencies. 592 * Destroy a given page along with its dependencies.
578 * @param $page 593 * @param $page
579 */ 594 */
580 - public function destroy($page) 595 + public function destroy(Page $page)
581 { 596 {
582 Activity::removeEntity($page); 597 Activity::removeEntity($page);
583 $page->views()->delete(); 598 $page->views()->delete();
599 + $page->tags()->delete();
584 $page->revisions()->delete(); 600 $page->revisions()->delete();
585 - $page->restrictions()->delete(); 601 + $page->permissions()->delete();
602 + $this->permissionService->deleteJointPermissionsForEntity($page);
586 $page->delete(); 603 $page->delete();
587 } 604 }
588 605
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
2 2
3 3
4 use BookStack\Exceptions\PermissionsException; 4 use BookStack\Exceptions\PermissionsException;
5 -use BookStack\Permission; 5 +use BookStack\RolePermission;
6 use BookStack\Role; 6 use BookStack\Role;
7 +use BookStack\Services\PermissionService;
7 use Setting; 8 use Setting;
8 9
9 class PermissionsRepo 10 class PermissionsRepo
...@@ -11,16 +12,21 @@ class PermissionsRepo ...@@ -11,16 +12,21 @@ class PermissionsRepo
11 12
12 protected $permission; 13 protected $permission;
13 protected $role; 14 protected $role;
15 + protected $permissionService;
16 +
17 + protected $systemRoles = ['admin', 'public'];
14 18
15 /** 19 /**
16 * PermissionsRepo constructor. 20 * PermissionsRepo constructor.
17 - * @param $permission 21 + * @param RolePermission $permission
18 - * @param $role 22 + * @param Role $role
23 + * @param PermissionService $permissionService
19 */ 24 */
20 - public function __construct(Permission $permission, Role $role) 25 + public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
21 { 26 {
22 $this->permission = $permission; 27 $this->permission = $permission;
23 $this->role = $role; 28 $this->role = $role;
29 + $this->permissionService = $permissionService;
24 } 30 }
25 31
26 /** 32 /**
...@@ -29,7 +35,7 @@ class PermissionsRepo ...@@ -29,7 +35,7 @@ class PermissionsRepo
29 */ 35 */
30 public function getAllRoles() 36 public function getAllRoles()
31 { 37 {
32 - return $this->role->all(); 38 + return $this->role->where('hidden', '=', false)->get();
33 } 39 }
34 40
35 /** 41 /**
...@@ -39,7 +45,7 @@ class PermissionsRepo ...@@ -39,7 +45,7 @@ class PermissionsRepo
39 */ 45 */
40 public function getAllRolesExcept(Role $role) 46 public function getAllRolesExcept(Role $role)
41 { 47 {
42 - return $this->role->where('id', '!=', $role->id)->get(); 48 + return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
43 } 49 }
44 50
45 /** 51 /**
...@@ -69,6 +75,7 @@ class PermissionsRepo ...@@ -69,6 +75,7 @@ class PermissionsRepo
69 75
70 $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; 76 $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
71 $this->assignRolePermissions($role, $permissions); 77 $this->assignRolePermissions($role, $permissions);
78 + $this->permissionService->buildJointPermissionForRole($role);
72 return $role; 79 return $role;
73 } 80 }
74 81
...@@ -77,10 +84,14 @@ class PermissionsRepo ...@@ -77,10 +84,14 @@ class PermissionsRepo
77 * Ensure Admin role always has all permissions. 84 * Ensure Admin role always has all permissions.
78 * @param $roleId 85 * @param $roleId
79 * @param $roleData 86 * @param $roleData
87 + * @throws PermissionsException
80 */ 88 */
81 public function updateRole($roleId, $roleData) 89 public function updateRole($roleId, $roleData)
82 { 90 {
83 $role = $this->role->findOrFail($roleId); 91 $role = $this->role->findOrFail($roleId);
92 +
93 + if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
94 +
84 $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; 95 $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
85 $this->assignRolePermissions($role, $permissions); 96 $this->assignRolePermissions($role, $permissions);
86 97
...@@ -91,6 +102,7 @@ class PermissionsRepo ...@@ -91,6 +102,7 @@ class PermissionsRepo
91 102
92 $role->fill($roleData); 103 $role->fill($roleData);
93 $role->save(); 104 $role->save();
105 + $this->permissionService->buildJointPermissionForRole($role);
94 } 106 }
95 107
96 /** 108 /**
...@@ -122,8 +134,8 @@ class PermissionsRepo ...@@ -122,8 +134,8 @@ class PermissionsRepo
122 $role = $this->role->findOrFail($roleId); 134 $role = $this->role->findOrFail($roleId);
123 135
124 // Prevent deleting admin role or default registration role. 136 // Prevent deleting admin role or default registration role.
125 - if ($role->name === 'admin') { 137 + if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
126 - throw new PermissionsException('The admin role cannot be deleted'); 138 + throw new PermissionsException('This role is a system role and cannot be deleted');
127 } else if ($role->id == setting('registration-role')) { 139 } else if ($role->id == setting('registration-role')) {
128 throw new PermissionsException('This role cannot be deleted while set as the default registration role.'); 140 throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
129 } 141 }
...@@ -136,6 +148,7 @@ class PermissionsRepo ...@@ -136,6 +148,7 @@ class PermissionsRepo
136 } 148 }
137 } 149 }
138 150
151 + $this->permissionService->deleteJointPermissionsForRole($role);
139 $role->delete(); 152 $role->delete();
140 } 153 }
141 154
......
1 +<?php namespace BookStack\Repos;
2 +
3 +use BookStack\Tag;
4 +use BookStack\Entity;
5 +use BookStack\Services\PermissionService;
6 +
7 +/**
8 + * Class TagRepo
9 + * @package BookStack\Repos
10 + */
11 +class TagRepo
12 +{
13 +
14 + protected $tag;
15 + protected $entity;
16 + protected $permissionService;
17 +
18 + /**
19 + * TagRepo constructor.
20 + * @param Tag $attr
21 + * @param Entity $ent
22 + * @param PermissionService $ps
23 + */
24 + public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
25 + {
26 + $this->tag = $attr;
27 + $this->entity = $ent;
28 + $this->permissionService = $ps;
29 + }
30 +
31 + /**
32 + * Get an entity instance of its particular type.
33 + * @param $entityType
34 + * @param $entityId
35 + * @param string $action
36 + */
37 + public function getEntity($entityType, $entityId, $action = 'view')
38 + {
39 + $entityInstance = $this->entity->getEntityInstance($entityType);
40 + $searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
41 + $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
42 + return $searchQuery->first();
43 + }
44 +
45 + /**
46 + * Get all tags for a particular entity.
47 + * @param string $entityType
48 + * @param int $entityId
49 + * @return mixed
50 + */
51 + public function getForEntity($entityType, $entityId)
52 + {
53 + $entity = $this->getEntity($entityType, $entityId);
54 + if ($entity === null) return collect();
55 +
56 + return $entity->tags;
57 + }
58 +
59 + /**
60 + * Get tag name suggestions from scanning existing tag names.
61 + * @param $searchTerm
62 + * @return array
63 + */
64 + public function getNameSuggestions($searchTerm)
65 + {
66 + if ($searchTerm === '') return [];
67 + $query = $this->tag->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc');
68 + $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
69 + return $query->get(['name'])->pluck('name');
70 + }
71 +
72 + /**
73 + * Get tag value suggestions from scanning existing tag values.
74 + * @param $searchTerm
75 + * @return array
76 + */
77 + public function getValueSuggestions($searchTerm)
78 + {
79 + if ($searchTerm === '') return [];
80 + $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc');
81 + $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
82 + return $query->get(['value'])->pluck('value');
83 + }
84 + /**
85 + * Save an array of tags to an entity
86 + * @param Entity $entity
87 + * @param array $tags
88 + * @return array|\Illuminate\Database\Eloquent\Collection
89 + */
90 + public function saveTagsToEntity(Entity $entity, $tags = [])
91 + {
92 + $entity->tags()->delete();
93 + $newTags = [];
94 + foreach ($tags as $tag) {
95 + if (trim($tag['name']) === '') continue;
96 + $newTags[] = $this->newInstanceFromInput($tag);
97 + }
98 +
99 + return $entity->tags()->saveMany($newTags);
100 + }
101 +
102 + /**
103 + * Create a new Tag instance from user input.
104 + * @param $input
105 + * @return static
106 + */
107 + protected function newInstanceFromInput($input)
108 + {
109 + $name = trim($input['name']);
110 + $value = isset($input['value']) ? trim($input['value']) : '';
111 + // Any other modification or cleanup required can go here
112 + $values = ['name' => $name, 'value' => $value];
113 + return $this->tag->newInstance($values);
114 + }
115 +
116 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -52,6 +52,27 @@ class UserRepo ...@@ -52,6 +52,27 @@ class UserRepo
52 } 52 }
53 53
54 /** 54 /**
55 + * Get all the users with their permissions in a paginated format.
56 + * @param int $count
57 + * @param $sortData
58 + * @return \Illuminate\Database\Eloquent\Builder|static
59 + */
60 + public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
61 + {
62 + $query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
63 +
64 + if ($sortData['search']) {
65 + $term = '%' . $sortData['search'] . '%';
66 + $query->where(function($query) use ($term) {
67 + $query->where('name', 'like', $term)
68 + ->orWhere('email', 'like', $term);
69 + });
70 + }
71 +
72 + return $query->paginate($count);
73 + }
74 +
75 + /**
55 * Creates a new user and attaches a role to them. 76 * Creates a new user and attaches a role to them.
56 * @param array $data 77 * @param array $data
57 * @return User 78 * @return User
...@@ -169,13 +190,22 @@ class UserRepo ...@@ -169,13 +190,22 @@ class UserRepo
169 } 190 }
170 191
171 /** 192 /**
193 + * Get the roles in the system that are assignable to a user.
194 + * @return mixed
195 + */
196 + public function getAssignableRoles()
197 + {
198 + return $this->role->visible();
199 + }
200 +
201 + /**
172 * Get all the roles which can be given restricted access to 202 * Get all the roles which can be given restricted access to
173 * other entities in the system. 203 * other entities in the system.
174 * @return mixed 204 * @return mixed
175 */ 205 */
176 public function getRestrictableRoles() 206 public function getRestrictableRoles()
177 { 207 {
178 - return $this->role->where('name', '!=', 'admin')->get(); 208 + return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
179 } 209 }
180 210
181 } 211 }
...\ No newline at end of file ...\ No newline at end of file
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 -
5 -use Illuminate\Database\Eloquent\Model;
6 3
7 class Role extends Model 4 class Role extends Model
8 { 5 {
...@@ -14,40 +11,54 @@ class Role extends Model ...@@ -14,40 +11,54 @@ class Role extends Model
14 */ 11 */
15 public function users() 12 public function users()
16 { 13 {
17 - return $this->belongsToMany('BookStack\User'); 14 + return $this->belongsToMany(User::class);
15 + }
16 +
17 + /**
18 + * Get all related JointPermissions.
19 + * @return \Illuminate\Database\Eloquent\Relations\HasMany
20 + */
21 + public function jointPermissions()
22 + {
23 + return $this->hasMany(JointPermission::class);
18 } 24 }
19 25
20 /** 26 /**
21 - * The permissions that belong to the role. 27 + * The RolePermissions that belong to the role.
22 */ 28 */
23 public function permissions() 29 public function permissions()
24 { 30 {
25 - return $this->belongsToMany('BookStack\Permission'); 31 + return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
26 } 32 }
27 33
28 /** 34 /**
29 * Check if this role has a permission. 35 * Check if this role has a permission.
30 - * @param $permission 36 + * @param $permissionName
37 + * @return bool
31 */ 38 */
32 - public function hasPermission($permission) 39 + public function hasPermission($permissionName)
33 { 40 {
34 - return $this->permissions->pluck('name')->contains($permission); 41 + $permissions = $this->getRelationValue('permissions');
42 + foreach ($permissions as $permission) {
43 + if ($permission->getRawAttribute('name') === $permissionName) return true;
44 + }
45 + return false;
35 } 46 }
36 47
37 /** 48 /**
38 * Add a permission to this role. 49 * Add a permission to this role.
39 - * @param Permission $permission 50 + * @param RolePermission $permission
40 */ 51 */
41 - public function attachPermission(Permission $permission) 52 + public function attachPermission(RolePermission $permission)
42 { 53 {
43 $this->permissions()->attach($permission->id); 54 $this->permissions()->attach($permission->id);
44 } 55 }
45 56
46 /** 57 /**
47 * Detach a single permission from this role. 58 * Detach a single permission from this role.
48 - * @param Permission $permission 59 + * @param RolePermission $permission
49 */ 60 */
50 - public function detachPermission(Permission $permission) 61 + public function detachPermission(RolePermission $permission)
51 { 62 {
52 $this->permissions()->detach($permission->id); 63 $this->permissions()->detach($permission->id);
53 } 64 }
...@@ -61,4 +72,24 @@ class Role extends Model ...@@ -61,4 +72,24 @@ class Role extends Model
61 { 72 {
62 return static::where('name', '=', $roleName)->first(); 73 return static::where('name', '=', $roleName)->first();
63 } 74 }
75 +
76 + /**
77 + * Get the role object for the specified system role.
78 + * @param $roleName
79 + * @return mixed
80 + */
81 + public static function getSystemRole($roleName)
82 + {
83 + return static::where('system_name', '=', $roleName)->first();
84 + }
85 +
86 + /**
87 + * Get all visible roles
88 + * @return mixed
89 + */
90 + public static function visible()
91 + {
92 + return static::where('hidden', '=', false)->orderBy('name')->get();
93 + }
94 +
64 } 95 }
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 3
5 -use Illuminate\Database\Eloquent\Model; 4 +class RolePermission extends Model
6 -
7 -class Permission extends Model
8 { 5 {
9 /** 6 /**
10 * The roles that belong to the permission. 7 * The roles that belong to the permission.
11 */ 8 */
12 public function roles() 9 public function roles()
13 { 10 {
14 - return $this->belongsToMany('BookStack\Role'); 11 + return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
15 } 12 }
16 13
17 /** 14 /**
18 * Get the permission object by name. 15 * Get the permission object by name.
19 - * @param $roleName 16 + * @param $name
20 * @return mixed 17 * @return mixed
21 */ 18 */
22 public static function getByName($name) 19 public static function getByName($name)
......
...@@ -8,17 +8,17 @@ class ActivityService ...@@ -8,17 +8,17 @@ class ActivityService
8 { 8 {
9 protected $activity; 9 protected $activity;
10 protected $user; 10 protected $user;
11 - protected $restrictionService; 11 + protected $permissionService;
12 12
13 /** 13 /**
14 * ActivityService constructor. 14 * ActivityService constructor.
15 * @param Activity $activity 15 * @param Activity $activity
16 - * @param RestrictionService $restrictionService 16 + * @param PermissionService $permissionService
17 */ 17 */
18 - public function __construct(Activity $activity, RestrictionService $restrictionService) 18 + public function __construct(Activity $activity, PermissionService $permissionService)
19 { 19 {
20 $this->activity = $activity; 20 $this->activity = $activity;
21 - $this->restrictionService = $restrictionService; 21 + $this->permissionService = $permissionService;
22 $this->user = auth()->user(); 22 $this->user = auth()->user();
23 } 23 }
24 24
...@@ -88,7 +88,7 @@ class ActivityService ...@@ -88,7 +88,7 @@ class ActivityService
88 */ 88 */
89 public function latest($count = 20, $page = 0) 89 public function latest($count = 20, $page = 0)
90 { 90 {
91 - $activityList = $this->restrictionService 91 + $activityList = $this->permissionService
92 ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') 92 ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
93 ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); 93 ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
94 94
...@@ -105,8 +105,16 @@ class ActivityService ...@@ -105,8 +105,16 @@ class ActivityService
105 */ 105 */
106 public function entityActivity($entity, $count = 20, $page = 0) 106 public function entityActivity($entity, $count = 20, $page = 0)
107 { 107 {
108 - $activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc') 108 + if ($entity->isA('book')) {
109 - ->skip($count * $page)->take($count)->get(); 109 + $query = $this->activity->where('book_id', '=', $entity->id);
110 + } else {
111 + $query = $this->activity->where('entity_type', '=', get_class($entity))
112 + ->where('entity_id', '=', $entity->id);
113 + }
114 +
115 + $activity = $this->permissionService
116 + ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
117 + ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
110 118
111 return $this->filterSimilar($activity); 119 return $this->filterSimilar($activity);
112 } 120 }
...@@ -121,7 +129,7 @@ class ActivityService ...@@ -121,7 +129,7 @@ class ActivityService
121 */ 129 */
122 public function userActivity($user, $count = 20, $page = 0) 130 public function userActivity($user, $count = 20, $page = 0)
123 { 131 {
124 - $activityList = $this->restrictionService 132 + $activityList = $this->permissionService
125 ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') 133 ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
126 ->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get(); 134 ->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
127 return $this->filterSimilar($activityList); 135 return $this->filterSimilar($activityList);
......
...@@ -34,6 +34,17 @@ class Ldap ...@@ -34,6 +34,17 @@ class Ldap
34 } 34 }
35 35
36 /** 36 /**
37 + * Set the version number for the given ldap connection.
38 + * @param $ldapConnection
39 + * @param $version
40 + * @return bool
41 + */
42 + public function setVersion($ldapConnection, $version)
43 + {
44 + return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
45 + }
46 +
47 + /**
37 * Search LDAP tree using the provided filter. 48 * Search LDAP tree using the provided filter.
38 * @param resource $ldapConnection 49 * @param resource $ldapConnection
39 * @param string $baseDn 50 * @param string $baseDn
......
...@@ -122,7 +122,7 @@ class LdapService ...@@ -122,7 +122,7 @@ class LdapService
122 122
123 // Set any required options 123 // Set any required options
124 if ($this->config['version']) { 124 if ($this->config['version']) {
125 - $this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']); 125 + $this->ldap->setVersion($ldapConnection, $this->config['version']);
126 } 126 }
127 127
128 $this->ldapConnection = $ldapConnection; 128 $this->ldapConnection = $ldapConnection;
......
1 <?php namespace BookStack\Services; 1 <?php namespace BookStack\Services;
2 2
3 -use GuzzleHttp\Exception\ClientException;
4 use Laravel\Socialite\Contracts\Factory as Socialite; 3 use Laravel\Socialite\Contracts\Factory as Socialite;
5 use BookStack\Exceptions\SocialDriverNotConfigured; 4 use BookStack\Exceptions\SocialDriverNotConfigured;
6 use BookStack\Exceptions\SocialSignInException; 5 use BookStack\Exceptions\SocialSignInException;
7 use BookStack\Exceptions\UserRegistrationException; 6 use BookStack\Exceptions\UserRegistrationException;
8 -use BookStack\Http\Controllers\Auth\AuthController;
9 use BookStack\Repos\UserRepo; 7 use BookStack\Repos\UserRepo;
10 use BookStack\SocialAccount; 8 use BookStack\SocialAccount;
11 -use BookStack\User;
12 9
13 class SocialAuthService 10 class SocialAuthService
14 { 11 {
......
...@@ -8,18 +8,18 @@ class ViewService ...@@ -8,18 +8,18 @@ class ViewService
8 8
9 protected $view; 9 protected $view;
10 protected $user; 10 protected $user;
11 - protected $restrictionService; 11 + protected $permissionService;
12 12
13 /** 13 /**
14 * ViewService constructor. 14 * ViewService constructor.
15 * @param View $view 15 * @param View $view
16 - * @param RestrictionService $restrictionService 16 + * @param PermissionService $permissionService
17 */ 17 */
18 - public function __construct(View $view, RestrictionService $restrictionService) 18 + public function __construct(View $view, PermissionService $permissionService)
19 { 19 {
20 $this->view = $view; 20 $this->view = $view;
21 $this->user = auth()->user(); 21 $this->user = auth()->user();
22 - $this->restrictionService = $restrictionService; 22 + $this->permissionService = $permissionService;
23 } 23 }
24 24
25 /** 25 /**
...@@ -55,7 +55,7 @@ class ViewService ...@@ -55,7 +55,7 @@ class ViewService
55 public function getPopular($count = 10, $page = 0, $filterModel = false) 55 public function getPopular($count = 10, $page = 0, $filterModel = false)
56 { 56 {
57 $skipCount = $count * $page; 57 $skipCount = $count * $page;
58 - $query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type') 58 + $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
59 ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) 59 ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
60 ->groupBy('viewable_id', 'viewable_type') 60 ->groupBy('viewable_id', 'viewable_type')
61 ->orderBy('view_count', 'desc'); 61 ->orderBy('view_count', 'desc');
...@@ -76,7 +76,7 @@ class ViewService ...@@ -76,7 +76,7 @@ class ViewService
76 { 76 {
77 if ($this->user === null) return collect(); 77 if ($this->user === null) return collect();
78 78
79 - $query = $this->restrictionService 79 + $query = $this->permissionService
80 ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); 80 ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
81 81
82 if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); 82 if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
......
1 -<?php 1 +<?php namespace BookStack;
2 -
3 -namespace BookStack;
4 -
5 -use Illuminate\Database\Eloquent\Model;
6 2
7 class Setting extends Model 3 class Setting extends Model
8 { 4 {
......
1 -<?php 1 +<?php namespace BookStack;
2 2
3 -namespace BookStack;
4 -
5 -use Illuminate\Database\Eloquent\Model;
6 3
7 class SocialAccount extends Model 4 class SocialAccount extends Model
8 { 5 {
...@@ -11,6 +8,6 @@ class SocialAccount extends Model ...@@ -11,6 +8,6 @@ class SocialAccount extends Model
11 8
12 public function user() 9 public function user()
13 { 10 {
14 - return $this->belongsTo('BookStack\User'); 11 + return $this->belongsTo(User::class);
15 } 12 }
16 } 13 }
......
1 +<?php namespace BookStack;
2 +
3 +/**
4 + * Class Attribute
5 + * @package BookStack
6 + */
7 +class Tag extends Model
8 +{
9 + protected $fillable = ['name', 'value', 'order'];
10 +
11 + /**
12 + * Get the entity that this tag belongs to
13 + * @return \Illuminate\Database\Eloquent\Relations\MorphTo
14 + */
15 + public function entity()
16 + {
17 + return $this->morphTo('entity');
18 + }
19 +}
...\ No newline at end of file ...\ No newline at end of file
1 -<?php 1 +<?php namespace BookStack;
2 -
3 -namespace BookStack;
4 2
5 use Illuminate\Auth\Authenticatable; 3 use Illuminate\Auth\Authenticatable;
6 -use Illuminate\Database\Eloquent\Model;
7 use Illuminate\Auth\Passwords\CanResetPassword; 4 use Illuminate\Auth\Passwords\CanResetPassword;
8 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; 5 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
9 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; 6 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
...@@ -52,7 +49,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -52,7 +49,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
52 */ 49 */
53 public function roles() 50 public function roles()
54 { 51 {
55 - return $this->belongsToMany('BookStack\Role'); 52 + return $this->belongsToMany(Role::class);
56 } 53 }
57 54
58 /** 55 /**
...@@ -116,7 +113,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -116,7 +113,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
116 */ 113 */
117 public function socialAccounts() 114 public function socialAccounts()
118 { 115 {
119 - return $this->hasMany('BookStack\SocialAccount'); 116 + return $this->hasMany(SocialAccount::class);
120 } 117 }
121 118
122 /** 119 /**
...@@ -151,7 +148,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ...@@ -151,7 +148,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
151 */ 148 */
152 public function avatar() 149 public function avatar()
153 { 150 {
154 - return $this->belongsTo('BookStack\Image', 'image_id'); 151 + return $this->belongsTo(Image::class, 'image_id');
155 } 152 }
156 153
157 /** 154 /**
......
1 -<?php 1 +<?php namespace BookStack;
2 -
3 -namespace BookStack;
4 -
5 -use Illuminate\Database\Eloquent\Model;
6 2
7 class View extends Model 3 class View extends Model
8 { 4 {
......
...@@ -31,7 +31,7 @@ if (!function_exists('versioned_asset')) { ...@@ -31,7 +31,7 @@ if (!function_exists('versioned_asset')) {
31 31
32 /** 32 /**
33 * Check if the current user has a permission. 33 * Check if the current user has a permission.
34 - * If an ownable element is passed in the permissions are checked against 34 + * If an ownable element is passed in the jointPermissions are checked against
35 * that particular item. 35 * that particular item.
36 * @param $permission 36 * @param $permission
37 * @param \BookStack\Ownable $ownable 37 * @param \BookStack\Ownable $ownable
...@@ -39,26 +39,13 @@ if (!function_exists('versioned_asset')) { ...@@ -39,26 +39,13 @@ if (!function_exists('versioned_asset')) {
39 */ 39 */
40 function userCan($permission, \BookStack\Ownable $ownable = null) 40 function userCan($permission, \BookStack\Ownable $ownable = null)
41 { 41 {
42 - if (!auth()->check()) return false;
43 if ($ownable === null) { 42 if ($ownable === null) {
44 return auth()->user() && auth()->user()->can($permission); 43 return auth()->user() && auth()->user()->can($permission);
45 } 44 }
46 45
47 // Check permission on ownable item 46 // Check permission on ownable item
48 - $permissionBaseName = strtolower($permission) . '-'; 47 + $permissionService = app('BookStack\Services\PermissionService');
49 - $hasPermission = false; 48 + return $permissionService->checkEntityUserAccess($ownable, $permission);
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 entity
56 - $restrictionService = app('BookStack\Services\RestrictionService');
57 - $explodedPermission = explode('-', $permission);
58 - $action = end($explodedPermission);
59 - $hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
60 - $restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
61 - return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
62 } 49 }
63 50
64 /** 51 /**
...@@ -72,3 +59,35 @@ function setting($key, $default = false) ...@@ -72,3 +59,35 @@ function setting($key, $default = false)
72 $settingService = app('BookStack\Services\SettingService'); 59 $settingService = app('BookStack\Services\SettingService');
73 return $settingService->get($key, $default); 60 return $settingService->get($key, $default);
74 } 61 }
62 +
63 +/**
64 + * Generate a url with multiple parameters for sorting purposes.
65 + * Works out the logic to set the correct sorting direction
66 + * Discards empty parameters and allows overriding.
67 + * @param $path
68 + * @param array $data
69 + * @param array $overrideData
70 + * @return string
71 + */
72 +function sortUrl($path, $data, $overrideData = [])
73 +{
74 + $queryStringSections = [];
75 + $queryData = array_merge($data, $overrideData);
76 +
77 + // Change sorting direction is already sorted on current attribute
78 + if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
79 + $queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
80 + } else {
81 + $queryData['order'] = 'asc';
82 + }
83 +
84 + foreach ($queryData as $name => $value) {
85 + $trimmedVal = trim($value);
86 + if ($trimmedVal === '') continue;
87 + $queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
88 + }
89 +
90 + if (count($queryStringSections) === 0) return $path;
91 +
92 + return $path . '?' . implode('&', $queryStringSections);
93 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -84,8 +84,8 @@ return [ ...@@ -84,8 +84,8 @@ return [
84 'driver' => 'mysql', 84 'driver' => 'mysql',
85 'host' => 'localhost', 85 'host' => 'localhost',
86 'database' => 'bookstack-test', 86 'database' => 'bookstack-test',
87 - 'username' => 'bookstack-test', 87 + 'username' => env('MYSQL_USER', 'bookstack-test'),
88 - 'password' => 'bookstack-test', 88 + 'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
89 'charset' => 'utf8', 89 'charset' => 'utf8',
90 'collation' => 'utf8_unicode_ci', 90 'collation' => 'utf8_unicode_ci',
91 'prefix' => '', 91 'prefix' => '',
......
...@@ -52,4 +52,11 @@ $factory->define(BookStack\Role::class, function ($faker) { ...@@ -52,4 +52,11 @@ $factory->define(BookStack\Role::class, function ($faker) {
52 'display_name' => $faker->sentence(3), 52 'display_name' => $faker->sentence(3),
53 'description' => $faker->sentence(10) 53 'description' => $faker->sentence(10)
54 ]; 54 ];
55 +});
56 +
57 +$factory->define(BookStack\Tag::class, function ($faker) {
58 + return [
59 + 'name' => $faker->city,
60 + 'value' => $faker->sentence(3)
61 + ];
55 }); 62 });
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -21,10 +21,13 @@ class CreateUsersTable extends Migration ...@@ -21,10 +21,13 @@ class CreateUsersTable extends Migration
21 $table->nullableTimestamps(); 21 $table->nullableTimestamps();
22 }); 22 });
23 23
24 - \BookStack\User::forceCreate([ 24 + // Create the initial admin user
25 + DB::table('users')->insert([
25 'name' => 'Admin', 26 'name' => 'Admin',
26 'email' => 'admin@admin.com', 27 'email' => 'admin@admin.com',
27 - 'password' => bcrypt('password') 28 + 'password' => bcrypt('password'),
29 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
30 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
28 ]); 31 ]);
29 } 32 }
30 33
......
...@@ -68,35 +68,44 @@ class AddRolesAndPermissions extends Migration ...@@ -68,35 +68,44 @@ class AddRolesAndPermissions extends Migration
68 68
69 69
70 // Create default roles 70 // Create default roles
71 - $admin = new \BookStack\Role(); 71 + $adminId = DB::table('roles')->insertGetId([
72 - $admin->name = 'admin'; 72 + 'name' => 'admin',
73 - $admin->display_name = 'Admin'; 73 + 'display_name' => 'Admin',
74 - $admin->description = 'Administrator of the whole application'; 74 + 'description' => 'Administrator of the whole application',
75 - $admin->save(); 75 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
76 - 76 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
77 - $editor = new \BookStack\Role(); 77 + ]);
78 - $editor->name = 'editor'; 78 + $editorId = DB::table('roles')->insertGetId([
79 - $editor->display_name = 'Editor'; 79 + 'name' => 'editor',
80 - $editor->description = 'User can edit Books, Chapters & Pages'; 80 + 'display_name' => 'Editor',
81 - $editor->save(); 81 + 'description' => 'User can edit Books, Chapters & Pages',
82 - 82 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
83 - $viewer = new \BookStack\Role(); 83 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
84 - $viewer->name = 'viewer'; 84 + ]);
85 - $viewer->display_name = 'Viewer'; 85 + $viewerId = DB::table('roles')->insertGetId([
86 - $viewer->description = 'User can view books & their content behind authentication'; 86 + 'name' => 'viewer',
87 - $viewer->save(); 87 + 'display_name' => 'Viewer',
88 + 'description' => 'User can view books & their content behind authentication',
89 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
90 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
91 + ]);
92 +
88 93
89 // Create default CRUD permissions and allocate to admins and editors 94 // Create default CRUD permissions and allocate to admins and editors
90 $entities = ['Book', 'Page', 'Chapter', 'Image']; 95 $entities = ['Book', 'Page', 'Chapter', 'Image'];
91 $ops = ['Create', 'Update', 'Delete']; 96 $ops = ['Create', 'Update', 'Delete'];
92 foreach ($entities as $entity) { 97 foreach ($entities as $entity) {
93 foreach ($ops as $op) { 98 foreach ($ops as $op) {
94 - $newPermission = new \BookStack\Permission(); 99 + $newPermId = DB::table('permissions')->insertGetId([
95 - $newPermission->name = strtolower($entity) . '-' . strtolower($op); 100 + 'name' => strtolower($entity) . '-' . strtolower($op),
96 - $newPermission->display_name = $op . ' ' . $entity . 's'; 101 + 'display_name' => $op . ' ' . $entity . 's',
97 - $newPermission->save(); 102 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
98 - $admin->attachPermission($newPermission); 103 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
99 - $editor->attachPermission($newPermission); 104 + ]);
105 + DB::table('permission_role')->insert([
106 + ['permission_id' => $newPermId, 'role_id' => $adminId],
107 + ['permission_id' => $newPermId, 'role_id' => $editorId]
108 + ]);
100 } 109 }
101 } 110 }
102 111
...@@ -105,19 +114,27 @@ class AddRolesAndPermissions extends Migration ...@@ -105,19 +114,27 @@ class AddRolesAndPermissions extends Migration
105 $ops = ['Create', 'Update', 'Delete']; 114 $ops = ['Create', 'Update', 'Delete'];
106 foreach ($entities as $entity) { 115 foreach ($entities as $entity) {
107 foreach ($ops as $op) { 116 foreach ($ops as $op) {
108 - $newPermission = new \BookStack\Permission(); 117 + $newPermId = DB::table('permissions')->insertGetId([
109 - $newPermission->name = strtolower($entity) . '-' . strtolower($op); 118 + 'name' => strtolower($entity) . '-' . strtolower($op),
110 - $newPermission->display_name = $op . ' ' . $entity; 119 + 'display_name' => $op . ' ' . $entity,
111 - $newPermission->save(); 120 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
112 - $admin->attachPermission($newPermission); 121 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
122 + ]);
123 + DB::table('permission_role')->insert([
124 + 'permission_id' => $newPermId,
125 + 'role_id' => $adminId
126 + ]);
113 } 127 }
114 } 128 }
115 129
116 // Set all current users as admins 130 // Set all current users as admins
117 // (At this point only the initially create user should be an admin) 131 // (At this point only the initially create user should be an admin)
118 - $users = \BookStack\User::all(); 132 + $users = DB::table('users')->get();
119 foreach ($users as $user) { 133 foreach ($users as $user) {
120 - $user->attachRole($admin); 134 + DB::table('role_user')->insert([
135 + 'role_id' => $adminId,
136 + 'user_id' => $user->id
137 + ]);
121 } 138 }
122 139
123 } 140 }
......
...@@ -13,29 +13,31 @@ class UpdatePermissionsAndRoles extends Migration ...@@ -13,29 +13,31 @@ class UpdatePermissionsAndRoles extends Migration
13 public function up() 13 public function up()
14 { 14 {
15 // Get roles with permissions we need to change 15 // Get roles with permissions we need to change
16 - $adminRole = \BookStack\Role::getRole('admin'); 16 + $adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
17 - $editorRole = \BookStack\Role::getRole('editor'); 17 + $editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
18 18
19 // Delete old permissions 19 // Delete old permissions
20 - $permissions = \BookStack\Permission::all(); 20 + $permissions = DB::table('permissions')->delete();
21 - $permissions->each(function ($permission) {
22 - $permission->delete();
23 - });
24 21
25 // Create & attach new admin permissions 22 // Create & attach new admin permissions
26 $permissionsToCreate = [ 23 $permissionsToCreate = [
27 'settings-manage' => 'Manage Settings', 24 'settings-manage' => 'Manage Settings',
28 'users-manage' => 'Manage Users', 25 'users-manage' => 'Manage Users',
29 'user-roles-manage' => 'Manage Roles & Permissions', 26 'user-roles-manage' => 'Manage Roles & Permissions',
30 - 'restrictions-manage-all' => 'Manage All Entity Restrictions', 27 + 'restrictions-manage-all' => 'Manage All Entity Permissions',
31 - 'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content' 28 + 'restrictions-manage-own' => 'Manage Entity Permissions On Own Content'
32 ]; 29 ];
33 foreach ($permissionsToCreate as $name => $displayName) { 30 foreach ($permissionsToCreate as $name => $displayName) {
34 - $newPermission = new \BookStack\Permission(); 31 + $permissionId = DB::table('permissions')->insertGetId([
35 - $newPermission->name = $name; 32 + 'name' => $name,
36 - $newPermission->display_name = $displayName; 33 + 'display_name' => $displayName,
37 - $newPermission->save(); 34 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
38 - $adminRole->attachPermission($newPermission); 35 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
36 + ]);
37 + DB::table('permission_role')->insert([
38 + 'role_id' => $adminRoleId,
39 + 'permission_id' => $permissionId
40 + ]);
39 } 41 }
40 42
41 // Create & attach new entity permissions 43 // Create & attach new entity permissions
...@@ -43,12 +45,22 @@ class UpdatePermissionsAndRoles extends Migration ...@@ -43,12 +45,22 @@ class UpdatePermissionsAndRoles extends Migration
43 $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; 45 $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
44 foreach ($entities as $entity) { 46 foreach ($entities as $entity) {
45 foreach ($ops as $op) { 47 foreach ($ops as $op) {
46 - $newPermission = new \BookStack\Permission(); 48 + $permissionId = DB::table('permissions')->insertGetId([
47 - $newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); 49 + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
48 - $newPermission->display_name = $op . ' ' . $entity . 's'; 50 + 'display_name' => $op . ' ' . $entity . 's',
49 - $newPermission->save(); 51 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
50 - $adminRole->attachPermission($newPermission); 52 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
51 - if ($editorRole !== null) $editorRole->attachPermission($newPermission); 53 + ]);
54 + DB::table('permission_role')->insert([
55 + 'role_id' => $adminRoleId,
56 + 'permission_id' => $permissionId
57 + ]);
58 + if ($editorRole !== null) {
59 + DB::table('permission_role')->insert([
60 + 'role_id' => $editorRole->id,
61 + 'permission_id' => $permissionId
62 + ]);
63 + }
52 } 64 }
53 } 65 }
54 66
...@@ -62,24 +74,26 @@ class UpdatePermissionsAndRoles extends Migration ...@@ -62,24 +74,26 @@ class UpdatePermissionsAndRoles extends Migration
62 public function down() 74 public function down()
63 { 75 {
64 // Get roles with permissions we need to change 76 // Get roles with permissions we need to change
65 - $adminRole = \BookStack\Role::getRole('admin'); 77 + $adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
66 78
67 // Delete old permissions 79 // Delete old permissions
68 - $permissions = \BookStack\Permission::all(); 80 + $permissions = DB::table('permissions')->delete();
69 - $permissions->each(function ($permission) {
70 - $permission->delete();
71 - });
72 81
73 // Create default CRUD permissions and allocate to admins and editors 82 // Create default CRUD permissions and allocate to admins and editors
74 $entities = ['Book', 'Page', 'Chapter', 'Image']; 83 $entities = ['Book', 'Page', 'Chapter', 'Image'];
75 $ops = ['Create', 'Update', 'Delete']; 84 $ops = ['Create', 'Update', 'Delete'];
76 foreach ($entities as $entity) { 85 foreach ($entities as $entity) {
77 foreach ($ops as $op) { 86 foreach ($ops as $op) {
78 - $newPermission = new \BookStack\Permission(); 87 + $permissionId = DB::table('permissions')->insertGetId([
79 - $newPermission->name = strtolower($entity) . '-' . strtolower($op); 88 + 'name' => strtolower($entity) . '-' . strtolower($op),
80 - $newPermission->display_name = $op . ' ' . $entity . 's'; 89 + 'display_name' => $op . ' ' . $entity . 's',
81 - $newPermission->save(); 90 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
82 - $adminRole->attachPermission($newPermission); 91 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
92 + ]);
93 + DB::table('permission_role')->insert([
94 + 'role_id' => $adminRoleId,
95 + 'permission_id' => $permissionId
96 + ]);
83 } 97 }
84 } 98 }
85 99
...@@ -88,11 +102,16 @@ class UpdatePermissionsAndRoles extends Migration ...@@ -88,11 +102,16 @@ class UpdatePermissionsAndRoles extends Migration
88 $ops = ['Create', 'Update', 'Delete']; 102 $ops = ['Create', 'Update', 'Delete'];
89 foreach ($entities as $entity) { 103 foreach ($entities as $entity) {
90 foreach ($ops as $op) { 104 foreach ($ops as $op) {
91 - $newPermission = new \BookStack\Permission(); 105 + $permissionId = DB::table('permissions')->insertGetId([
92 - $newPermission->name = strtolower($entity) . '-' . strtolower($op); 106 + 'name' => strtolower($entity) . '-' . strtolower($op),
93 - $newPermission->display_name = $op . ' ' . $entity; 107 + 'display_name' => $op . ' ' . $entity,
94 - $newPermission->save(); 108 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
95 - $adminRole->attachPermission($newPermission); 109 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
110 + ]);
111 + DB::table('permission_role')->insert([
112 + 'role_id' => $adminRoleId,
113 + 'permission_id' => $permissionId
114 + ]);
96 } 115 }
97 } 116 }
98 } 117 }
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddViewPermissionsToRoles extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + $currentRoles = DB::table('roles')->get();
16 +
17 + // Create new view permission
18 + $entities = ['Book', 'Page', 'Chapter'];
19 + $ops = ['View All', 'View Own'];
20 + foreach ($entities as $entity) {
21 + foreach ($ops as $op) {
22 + $permId = DB::table('permissions')->insertGetId([
23 + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
24 + 'display_name' => $op . ' ' . $entity . 's',
25 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
26 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
27 + ]);
28 + // Assign view permission to all current roles
29 + foreach ($currentRoles as $role) {
30 + DB::table('permission_role')->insert([
31 + 'role_id' => $role->id,
32 + 'permission_id' => $permId
33 + ]);
34 + }
35 + }
36 + }
37 + }
38 +
39 + /**
40 + * Reverse the migrations.
41 + *
42 + * @return void
43 + */
44 + public function down()
45 + {
46 + // Delete the new view permission
47 + $entities = ['Book', 'Page', 'Chapter'];
48 + $ops = ['View All', 'View Own'];
49 + foreach ($entities as $entity) {
50 + foreach ($ops as $op) {
51 + $permissionName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
52 + $permission = DB::table('permissions')->where('name', '=', $permissionName)->first();
53 + DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete();
54 + DB::table('permissions')->where('name', '=', $permissionName)->delete();
55 + }
56 + }
57 + }
58 +}
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class CreateJointPermissionsTable extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::create('joint_permissions', function (Blueprint $table) {
16 + $table->increments('id');
17 + $table->integer('role_id');
18 + $table->string('entity_type');
19 + $table->integer('entity_id');
20 + $table->string('action');
21 + $table->boolean('has_permission')->default(false);
22 + $table->boolean('has_permission_own')->default(false);
23 + $table->integer('created_by');
24 + // Create indexes
25 + $table->index(['entity_id', 'entity_type']);
26 + $table->index('has_permission');
27 + $table->index('has_permission_own');
28 + $table->index('role_id');
29 + $table->index('action');
30 + $table->index('created_by');
31 + });
32 +
33 + Schema::table('roles', function (Blueprint $table) {
34 + $table->string('system_name');
35 + $table->boolean('hidden')->default(false);
36 + $table->index('hidden');
37 + $table->index('system_name');
38 + });
39 +
40 + Schema::rename('permissions', 'role_permissions');
41 + Schema::rename('restrictions', 'entity_permissions');
42 +
43 + // Create the new public role
44 + $publicRoleData = [
45 + 'name' => 'public',
46 + 'display_name' => 'Public',
47 + 'description' => 'The role given to public visitors if allowed',
48 + 'system_name' => 'public',
49 + 'hidden' => true,
50 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
51 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
52 + ];
53 +
54 + // Ensure unique name
55 + while (DB::table('roles')->where('name', '=', $publicRoleData['display_name'])->count() > 0) {
56 + $publicRoleData['display_name'] = $publicRoleData['display_name'] . str_random(2);
57 + }
58 + $publicRoleId = DB::table('roles')->insertGetId($publicRoleData);
59 +
60 + // Add new view permissions to public role
61 + $entities = ['Book', 'Page', 'Chapter'];
62 + $ops = ['View All', 'View Own'];
63 + foreach ($entities as $entity) {
64 + foreach ($ops as $op) {
65 + $name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
66 + $permission = DB::table('role_permissions')->where('name', '=', $name)->first();
67 + // Assign view permission to public
68 + DB::table('permission_role')->insert([
69 + 'permission_id' => $permission->id,
70 + 'role_id' => $publicRoleId
71 + ]);
72 + }
73 + }
74 +
75 + // Update admin role with system name
76 + DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
77 +
78 + // Generate the new entity jointPermissions
79 + $restrictionService = app(\BookStack\Services\PermissionService::class);
80 + $restrictionService->buildJointPermissions();
81 + }
82 +
83 + /**
84 + * Reverse the migrations.
85 + *
86 + * @return void
87 + */
88 + public function down()
89 + {
90 + Schema::drop('joint_permissions');
91 +
92 + Schema::rename('role_permissions', 'permissions');
93 + Schema::rename('entity_permissions', 'restrictions');
94 +
95 + // Delete the public role
96 + DB::table('roles')->where('system_name', '=', 'public')->delete();
97 +
98 + Schema::table('roles', function (Blueprint $table) {
99 + $table->dropColumn('system_name');
100 + $table->dropColumn('hidden');
101 + });
102 + }
103 +}
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class CreateTagsTable extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::create('tags', function (Blueprint $table) {
16 + $table->increments('id');
17 + $table->integer('entity_id');
18 + $table->string('entity_type', 100);
19 + $table->string('name');
20 + $table->string('value');
21 + $table->integer('order');
22 + $table->timestamps();
23 +
24 + $table->index('name');
25 + $table->index('value');
26 + $table->index('order');
27 + $table->index(['entity_id', 'entity_type']);
28 + });
29 + }
30 +
31 + /**
32 + * Reverse the migrations.
33 + *
34 + * @return void
35 + */
36 + public function down()
37 + {
38 + Schema::drop('tags');
39 + }
40 +}
...@@ -20,12 +20,15 @@ class DummyContentSeeder extends Seeder ...@@ -20,12 +20,15 @@ class DummyContentSeeder extends Seeder
20 ->each(function($book) use ($user) { 20 ->each(function($book) use ($user) {
21 $chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) 21 $chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
22 ->each(function($chapter) use ($user, $book){ 22 ->each(function($chapter) use ($user, $book){
23 - $pages = factory(\BookStack\Page::class, 10)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); 23 + $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
24 $chapter->pages()->saveMany($pages); 24 $chapter->pages()->saveMany($pages);
25 }); 25 });
26 $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]); 26 $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
27 $book->chapters()->saveMany($chapters); 27 $book->chapters()->saveMany($chapters);
28 $book->pages()->saveMany($pages); 28 $book->pages()->saveMany($pages);
29 }); 29 });
30 +
31 + $restrictionService = app(\BookStack\Services\PermissionService::class);
32 + $restrictionService->buildJointPermissions();
30 } 33 }
31 } 34 }
......
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
4 "gulp": "^3.9.0" 4 "gulp": "^3.9.0"
5 }, 5 },
6 "dependencies": { 6 "dependencies": {
7 - "angular": "^1.5.0-rc.0", 7 + "angular": "^1.5.5",
8 - "angular-animate": "^1.5.0-rc.0", 8 + "angular-animate": "^1.5.5",
9 - "angular-resource": "^1.5.0-rc.0", 9 + "angular-resource": "^1.5.5",
10 - "angular-sanitize": "^1.5.0-rc.0", 10 + "angular-sanitize": "^1.5.5",
11 + "angular-ui-sortable": "^0.14.0",
11 "babel-runtime": "^5.8.29", 12 "babel-runtime": "^5.8.29",
12 "bootstrap-sass": "^3.0.0", 13 "bootstrap-sass": "^3.0.0",
13 "dropzone": "^4.0.1", 14 "dropzone": "^4.0.1",
......
...@@ -34,5 +34,6 @@ ...@@ -34,5 +34,6 @@
34 <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/> 34 <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
35 <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/> 35 <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
36 <env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/> 36 <env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
37 + <env name="APP_URL" value="http://bookstack.dev"/>
37 </php> 38 </php>
38 </phpunit> 39 </phpunit>
......
1 # BookStack 1 # BookStack
2 2
3 +[![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](https://travis-ci.org/ssddanbrown/BookStack)
4 +
3 A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. 5 A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
4 6
5 * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation) 7 * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
......
...@@ -400,4 +400,116 @@ module.exports = function (ngApp, events) { ...@@ -400,4 +400,116 @@ module.exports = function (ngApp, events) {
400 400
401 }]); 401 }]);
402 402
403 -};
...\ No newline at end of file ...\ No newline at end of file
403 + ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
404 + function ($scope, $http, $attrs) {
405 +
406 + const pageId = Number($attrs.pageId);
407 + $scope.tags = [];
408 +
409 + $scope.sortOptions = {
410 + handle: '.handle',
411 + items: '> tr',
412 + containment: "parent",
413 + axis: "y"
414 + };
415 +
416 + /**
417 + * Push an empty tag to the end of the scope tags.
418 + */
419 + function addEmptyTag() {
420 + $scope.tags.push({
421 + name: '',
422 + value: ''
423 + });
424 + }
425 + $scope.addEmptyTag = addEmptyTag;
426 +
427 + /**
428 + * Get all tags for the current book and add into scope.
429 + */
430 + function getTags() {
431 + $http.get('/ajax/tags/get/page/' + pageId).then((responseData) => {
432 + $scope.tags = responseData.data;
433 + addEmptyTag();
434 + });
435 + }
436 + getTags();
437 +
438 + /**
439 + * Set the order property on all tags.
440 + */
441 + function setTagOrder() {
442 + for (let i = 0; i < $scope.tags.length; i++) {
443 + $scope.tags[i].order = i;
444 + }
445 + }
446 +
447 + /**
448 + * When an tag changes check if another empty editable
449 + * field needs to be added onto the end.
450 + * @param tag
451 + */
452 + $scope.tagChange = function(tag) {
453 + let cPos = $scope.tags.indexOf(tag);
454 + if (cPos !== $scope.tags.length-1) return;
455 +
456 + if (tag.name !== '' || tag.value !== '') {
457 + addEmptyTag();
458 + }
459 + };
460 +
461 + /**
462 + * When an tag field loses focus check the tag to see if its
463 + * empty and therefore could be removed from the list.
464 + * @param tag
465 + */
466 + $scope.tagBlur = function(tag) {
467 + let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
468 + if (tag.name === '' && tag.value === '' && !isLast) {
469 + let cPos = $scope.tags.indexOf(tag);
470 + $scope.tags.splice(cPos, 1);
471 + }
472 + };
473 +
474 + /**
475 + * Save the tags to the current page.
476 + */
477 + $scope.saveTags = function() {
478 + setTagOrder();
479 + let postData = {tags: $scope.tags};
480 + $http.post('/ajax/tags/update/page/' + pageId, postData).then((responseData) => {
481 + $scope.tags = responseData.data.tags;
482 + addEmptyTag();
483 + events.emit('success', responseData.data.message);
484 + })
485 + };
486 +
487 + /**
488 + * Remove a tag from the current list.
489 + * @param tag
490 + */
491 + $scope.removeTag = function(tag) {
492 + let cIndex = $scope.tags.indexOf(tag);
493 + $scope.tags.splice(cIndex, 1);
494 + };
495 +
496 + }]);
497 +
498 +};
499 +
500 +
501 +
502 +
503 +
504 +
505 +
506 +
507 +
508 +
509 +
510 +
511 +
512 +
513 +
514 +
515 +
......
...@@ -301,6 +301,219 @@ module.exports = function (ngApp, events) { ...@@ -301,6 +301,219 @@ module.exports = function (ngApp, events) {
301 301
302 } 302 }
303 } 303 }
304 - }]) 304 + }]);
305 +
306 + ngApp.directive('toolbox', [function() {
307 + return {
308 + restrict: 'A',
309 + link: function(scope, elem, attrs) {
310 +
311 + // Get common elements
312 + const $buttons = elem.find('[tab-button]');
313 + const $content = elem.find('[tab-content]');
314 + const $toggle = elem.find('[toolbox-toggle]');
315 +
316 + // Handle toolbox toggle click
317 + $toggle.click((e) => {
318 + elem.toggleClass('open');
319 + });
320 +
321 + // Set an active tab/content by name
322 + function setActive(tabName, openToolbox) {
323 + $buttons.removeClass('active');
324 + $content.hide();
325 + $buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
326 + $content.filter(`[tab-content="${tabName}"]`).show();
327 + if (openToolbox) elem.addClass('open');
328 + }
329 +
330 + // Set the first tab content active on load
331 + setActive($content.first().attr('tab-content'), false);
332 +
333 + // Handle tab button click
334 + $buttons.click(function(e) {
335 + let name = $(this).attr('tab-button');
336 + setActive(name, true);
337 + });
338 + }
339 + }
340 + }]);
341 +
342 + ngApp.directive('autosuggestions', ['$http', function($http) {
343 + return {
344 + restrict: 'A',
345 + link: function(scope, elem, attrs) {
346 +
347 + // Local storage for quick caching.
348 + const localCache = {};
349 +
350 + // Create suggestion element
351 + const suggestionBox = document.createElement('ul');
352 + suggestionBox.className = 'suggestion-box';
353 + suggestionBox.style.position = 'absolute';
354 + suggestionBox.style.display = 'none';
355 + const $suggestionBox = $(suggestionBox);
356 +
357 + // General state tracking
358 + let isShowing = false;
359 + let currentInput = false;
360 + let active = 0;
361 +
362 + // Listen to input events on autosuggest fields
363 + elem.on('input', '[autosuggest]', function(event) {
364 + let $input = $(this);
365 + let val = $input.val();
366 + let url = $input.attr('autosuggest');
367 + // No suggestions until at least 3 chars
368 + if (val.length < 3) {
369 + if (isShowing) {
370 + $suggestionBox.hide();
371 + isShowing = false;
372 + }
373 + return;
374 + };
375 +
376 + let suggestionPromise = getSuggestions(val.slice(0, 3), url);
377 + suggestionPromise.then((suggestions) => {
378 + if (val.length > 2) {
379 + suggestions = suggestions.filter((item) => {
380 + return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
381 + }).slice(0, 4);
382 + displaySuggestions($input, suggestions);
383 + }
384 + });
385 + });
386 +
387 + // Hide autosuggestions when input loses focus.
388 + // Slight delay to allow clicks.
389 + elem.on('blur', '[autosuggest]', function(event) {
390 + setTimeout(() => {
391 + $suggestionBox.hide();
392 + isShowing = false;
393 + }, 200)
394 + });
395 +
396 + elem.on('keydown', '[autosuggest]', function (event) {
397 + if (!isShowing) return;
398 +
399 + let suggestionElems = suggestionBox.childNodes;
400 + let suggestCount = suggestionElems.length;
401 +
402 + // Down arrow
403 + if (event.keyCode === 40) {
404 + let newActive = (active === suggestCount-1) ? 0 : active + 1;
405 + changeActiveTo(newActive, suggestionElems);
406 + }
407 + // Up arrow
408 + else if (event.keyCode === 38) {
409 + let newActive = (active === 0) ? suggestCount-1 : active - 1;
410 + changeActiveTo(newActive, suggestionElems);
411 + }
412 + // Enter key
413 + else if (event.keyCode === 13) {
414 + let text = suggestionElems[active].textContent;
415 + currentInput[0].value = text;
416 + currentInput.focus();
417 + $suggestionBox.hide();
418 + isShowing = false;
419 + event.preventDefault();
420 + return false;
421 + }
422 + });
423 +
424 + // Change the active suggestion to the given index
425 + function changeActiveTo(index, suggestionElems) {
426 + suggestionElems[active].className = '';
427 + active = index;
428 + suggestionElems[active].className = 'active';
429 + }
430 +
431 + // Display suggestions on a field
432 + let prevSuggestions = [];
433 + function displaySuggestions($input, suggestions) {
434 +
435 + // Hide if no suggestions
436 + if (suggestions.length === 0) {
437 + $suggestionBox.hide();
438 + isShowing = false;
439 + prevSuggestions = suggestions;
440 + return;
441 + }
442 +
443 + // Otherwise show and attach to input
444 + if (!isShowing) {
445 + $suggestionBox.show();
446 + isShowing = true;
447 + }
448 + if ($input !== currentInput) {
449 + $suggestionBox.detach();
450 + $input.after($suggestionBox);
451 + currentInput = $input;
452 + }
453 +
454 + // Return if no change
455 + if (prevSuggestions.join() === suggestions.join()) {
456 + prevSuggestions = suggestions;
457 + return;
458 + }
459 +
460 + // Build suggestions
461 + $suggestionBox[0].innerHTML = '';
462 + for (let i = 0; i < suggestions.length; i++) {
463 + var suggestion = document.createElement('li');
464 + suggestion.textContent = suggestions[i];
465 + suggestion.onclick = suggestionClick;
466 + if (i === 0) {
467 + suggestion.className = 'active'
468 + active = 0;
469 + };
470 + $suggestionBox[0].appendChild(suggestion);
471 + }
472 +
473 + prevSuggestions = suggestions;
474 + }
475 +
476 + // Suggestion click event
477 + function suggestionClick(event) {
478 + let text = this.textContent;
479 + currentInput[0].value = text;
480 + currentInput.focus();
481 + $suggestionBox.hide();
482 + isShowing = false;
483 + };
484 +
485 + // Get suggestions & cache
486 + function getSuggestions(input, url) {
487 + let searchUrl = url + '?search=' + encodeURIComponent(input);
488 +
489 + // Get from local cache if exists
490 + if (localCache[searchUrl]) {
491 + return new Promise((resolve, reject) => {
492 + resolve(localCache[input]);
493 + });
494 + }
495 +
496 + return $http.get(searchUrl).then((response) => {
497 + localCache[input] = response.data;
498 + return response.data;
499 + });
500 + }
501 +
502 + }
503 + }
504 + }]);
505 +};
506 +
507 +
508 +
509 +
510 +
511 +
512 +
513 +
514 +
515 +
516 +
517 +
518 +
305 519
306 -};
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -5,9 +5,9 @@ var angular = require('angular'); ...@@ -5,9 +5,9 @@ var angular = require('angular');
5 var ngResource = require('angular-resource'); 5 var ngResource = require('angular-resource');
6 var ngAnimate = require('angular-animate'); 6 var ngAnimate = require('angular-animate');
7 var ngSanitize = require('angular-sanitize'); 7 var ngSanitize = require('angular-sanitize');
8 +require('angular-ui-sortable');
8 9
9 -var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']); 10 +var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
10 -
11 11
12 // Global Event System 12 // Global Event System
13 var Events = { 13 var Events = {
......
...@@ -65,6 +65,9 @@ $button-border-radius: 2px; ...@@ -65,6 +65,9 @@ $button-border-radius: 2px;
65 &:focus, &:active { 65 &:focus, &:active {
66 outline: 0; 66 outline: 0;
67 } 67 }
68 + &:hover {
69 + text-decoration: none;
70 + }
68 &.neg { 71 &.neg {
69 color: $negative; 72 color: $negative;
70 } 73 }
......
...@@ -154,6 +154,11 @@ input:checked + .toggle-switch { ...@@ -154,6 +154,11 @@ input:checked + .toggle-switch {
154 154
155 .form-group { 155 .form-group {
156 margin-bottom: $-s; 156 margin-bottom: $-s;
157 + textarea {
158 + display: block;
159 + width: 100%;
160 + min-height: 64px;
161 + }
157 } 162 }
158 163
159 .form-group { 164 .form-group {
...@@ -239,6 +244,17 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] { ...@@ -239,6 +244,17 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
239 } 244 }
240 } 245 }
241 246
247 +input.outline {
248 + border: 0;
249 + border-bottom: 2px solid #DDD;
250 + border-radius: 0;
251 + &:focus, &:active {
252 + border: 0;
253 + border-bottom: 2px solid #AAA;
254 + outline: 0;
255 + }
256 +}
257 +
242 #login-form label[for="remember"] { 258 #login-form label[for="remember"] {
243 margin: 0; 259 margin: 0;
244 } 260 }
......
...@@ -11,13 +11,16 @@ body.flexbox { ...@@ -11,13 +11,16 @@ body.flexbox {
11 #content { 11 #content {
12 flex: 1; 12 flex: 1;
13 display: flex; 13 display: flex;
14 + min-height: 0px;
14 } 15 }
15 } 16 }
16 17
17 .flex-fill { 18 .flex-fill {
18 display: flex; 19 display: flex;
19 align-items: stretch; 20 align-items: stretch;
21 + min-height: 0px;
20 .flex, &.flex { 22 .flex, &.flex {
23 + min-height: 0px;
21 flex: 1; 24 flex: 1;
22 } 25 }
23 } 26 }
......
...@@ -266,6 +266,7 @@ ul.pagination { ...@@ -266,6 +266,7 @@ ul.pagination {
266 display: inline-block; 266 display: inline-block;
267 list-style: none; 267 list-style: none;
268 margin: $-m 0; 268 margin: $-m 0;
269 + padding-left: 1px;
269 li { 270 li {
270 float: left; 271 float: left;
271 } 272 }
...@@ -300,6 +301,10 @@ ul.pagination { ...@@ -300,6 +301,10 @@ ul.pagination {
300 } 301 }
301 } 302 }
302 303
304 +.compact ul.pagination {
305 + margin: 0;
306 +}
307 +
303 .entity-list { 308 .entity-list {
304 >div { 309 >div {
305 padding: $-m 0; 310 padding: $-m 0;
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
49 height:auto; 49 height:auto;
50 } 50 }
51 h1, h2, h3, h4, h5, h6 { 51 h1, h2, h3, h4, h5, h6 {
52 - clear: both; 52 + clear: left;
53 } 53 }
54 hr { 54 hr {
55 clear: both; 55 clear: both;
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
72 .pointer { 72 .pointer {
73 border: 1px solid #CCC; 73 border: 1px solid #CCC;
74 display: inline-block; 74 display: inline-block;
75 - padding: $-xs $-s; 75 + padding: $-s $-s;
76 border-radius: 4px; 76 border-radius: 4px;
77 box-shadow: 0 0 8px 1px rgba(212, 209, 209, 0.35); 77 box-shadow: 0 0 8px 1px rgba(212, 209, 209, 0.35);
78 position: absolute; 78 position: absolute;
...@@ -122,9 +122,181 @@ ...@@ -122,9 +122,181 @@
122 } 122 }
123 } 123 }
124 124
125 -h1, h2, h3, h4, h5, h6 { 125 +// Attribute form
126 - &:hover a.link-hook { 126 +.floating-toolbox {
127 - opacity: 1; 127 + background-color: #FFF;
128 - transform: translate3d(0, 0, 0); 128 + border: 1px solid #DDD;
129 + right: $-xl*2;
130 + z-index: 99;
131 + width: 48px;
132 + overflow: hidden;
133 + align-items: stretch;
134 + flex-direction: row;
135 + display: flex;
136 + transition: width ease-in-out 180ms;
137 + margin-top: -1px;
138 + min-height: 0px;
139 + &.open {
140 + width: 480px;
141 + }
142 + [toolbox-toggle] i {
143 + transition: transform ease-in-out 180ms;
144 + }
145 + [toolbox-toggle] {
146 + transition: background-color ease-in-out 180ms;
147 + }
148 + &.open [toolbox-toggle] {
149 + background-color: rgba(255, 0, 0, 0.29);
150 + }
151 + &.open [toolbox-toggle] i {
152 + transform: rotate(180deg);
153 + }
154 + > div {
155 + flex: 1;
156 + position: relative;
157 + }
158 + .tabs {
159 + display: block;
160 + border-right: 1px solid #DDD;
161 + width: 54px;
162 + flex: 0;
163 + }
164 + .tabs i {
165 + color: rgba(0, 0, 0, 0.5);
166 + padding: 0;
167 + margin: 0;
168 + }
169 + .tabs > span {
170 + display: block;
171 + cursor: pointer;
172 + padding: $-s $-m;
173 + font-size: 13.5px;
174 + line-height: 1.6;
175 + border-bottom: 1px solid rgba(255, 255, 255, 0.3);
176 + }
177 + &.open .tabs > span.active {
178 + color: #444;
179 + background-color: rgba(0, 0, 0, 0.1);
180 + }
181 + div[tab-content] {
182 + padding-bottom: 45px;
183 + display: flex;
184 + flex: 1;
185 + flex-direction: column;
186 + min-height: 0px;
187 + overflow-y: scroll;
188 + }
189 + div[tab-content] .padded {
190 + flex: 1;
191 + padding-top: 0;
192 + }
193 + h4 {
194 + font-size: 24px;
195 + margin: $-m 0 0 0;
196 + padding: 0 $-l $-s $-l;
197 + }
198 + .tags input {
199 + max-width: 100%;
200 + width: 100%;
201 + min-width: 50px;
202 + }
203 + .tags td {
204 + padding-right: $-s;
205 + padding-top: $-s;
206 + position: relative;
207 + }
208 + button.pos {
209 + position: absolute;
210 + bottom: 0;
211 + display: block;
212 + width: 100%;
213 + padding: $-s;
214 + height: 45px;
215 + border: 0;
216 + margin: 0;
217 + box-shadow: none;
218 + border-radius: 0;
219 + &:hover{
220 + box-shadow: none;
221 + }
222 + }
223 + .handle {
224 + user-select: none;
225 + cursor: move;
226 + color: #999;
227 + }
228 + form {
229 + display: flex;
230 + flex: 1;
231 + flex-direction: column;
232 + overflow-y: scroll;
233 + }
234 +}
235 +
236 +[tab-content] {
237 + display: none;
238 +}
239 +
240 +.tag-display {
241 + margin: $-xl $-xs;
242 + border: 1px solid #DDD;
243 + min-width: 180px;
244 + max-width: 320px;
245 + opacity: 0.7;
246 + z-index: 5;
247 + position: relative;
248 + table {
249 + width: 100%;
250 + margin: 0;
251 + padding: 0;
252 + }
253 + span {
254 + color: #666;
255 + margin-left: $-s;
256 + }
257 + .heading {
258 + padding: $-xs $-s;
259 + color: #444;
260 + }
261 + td {
262 + border: 0;
263 + border-bottom: 1px solid #DDD;
264 + padding: $-xs $-s;
265 + color: #444;
266 + }
267 + .tag-value {
268 + color: #888;
269 + }
270 + td i {
271 + color: #888;
272 + }
273 + tr:last-child td {
274 + border-bottom: none;
275 + }
276 + .tag {
277 + padding: $-s;
129 } 278 }
130 } 279 }
280 +
281 +.suggestion-box {
282 + position: absolute;
283 + background-color: #FFF;
284 + border: 1px solid #BBB;
285 + box-shadow: $bs-light;
286 + list-style: none;
287 + z-index: 100;
288 + padding: 0;
289 + margin: 0;
290 + border-radius: 3px;
291 + li {
292 + display: block;
293 + padding: $-xs $-s;
294 + border-bottom: 1px solid #DDD;
295 + &:last-child {
296 + border-bottom: 0;
297 + }
298 + &.active {
299 + background-color: #EEE;
300 + }
301 + }
302 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -26,6 +26,13 @@ table { ...@@ -26,6 +26,13 @@ table {
26 } 26 }
27 } 27 }
28 28
29 +table.no-style {
30 + td {
31 + border: 0;
32 + padding: 0;
33 + }
34 +}
35 +
29 table.list-table { 36 table.list-table {
30 margin: 0 -$-xs; 37 margin: 0 -$-xs;
31 td { 38 td {
......
...@@ -297,6 +297,12 @@ span.sep { ...@@ -297,6 +297,12 @@ span.sep {
297 display: block; 297 display: block;
298 } 298 }
299 299
300 +.action-header {
301 + h1 {
302 + margin-top: $-m;
303 + }
304 +}
305 +
300 /** 306 /**
301 * Icons 307 * Icons
302 */ 308 */
......
...@@ -21,6 +21,11 @@ ...@@ -21,6 +21,11 @@
21 21
22 [ng\:cloak], [ng-cloak], .ng-cloak { 22 [ng\:cloak], [ng-cloak], .ng-cloak {
23 display: none !important; 23 display: none !important;
24 + user-select: none;
25 +}
26 +
27 +[ng-click] {
28 + cursor: pointer;
24 } 29 }
25 30
26 // Jquery Sortable Styles 31 // Jquery Sortable Styles
...@@ -201,4 +206,4 @@ $btt-size: 40px; ...@@ -201,4 +206,4 @@ $btt-size: 40px;
201 background-color: $negative; 206 background-color: $negative;
202 color: #EEE; 207 color: #EEE;
203 } 208 }
204 -}
...\ No newline at end of file ...\ No newline at end of file
209 +}
......
...@@ -15,10 +15,16 @@ ...@@ -15,10 +15,16 @@
15 15
16 <!-- Scripts --> 16 <!-- Scripts -->
17 <script src="/libs/jquery/jquery.min.js?version=2.1.4"></script> 17 <script src="/libs/jquery/jquery.min.js?version=2.1.4"></script>
18 + <script src="/libs/jquery/jquery-ui.min.js?version=1.11.4"></script>
18 19
19 @yield('head') 20 @yield('head')
20 21
21 @include('partials/custom-styles') 22 @include('partials/custom-styles')
23 +
24 + <!-- Custom user content -->
25 + @if(setting('app-custom-head', false))
26 + {!! setting('app-custom-head') !!}
27 + @endif
22 </head> 28 </head>
23 <body class="@yield('body-class')" ng-app="bookStack"> 29 <body class="@yield('body-class')" ng-app="bookStack">
24 30
......
...@@ -68,9 +68,9 @@ ...@@ -68,9 +68,9 @@
68 <hr> 68 <hr>
69 @endif 69 @endif
70 <p class="text-muted small"> 70 <p class="text-muted small">
71 - Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif 71 + Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by <a href="/user/{{ $book->createdBy->id }}">{{$book->createdBy->name}}</a> @endif
72 <br> 72 <br>
73 - Last Updated {{$book->updated_at->diffForHumans()}} @if($book->updatedBy) by {{$book->updatedBy->name}} @endif 73 + Last Updated {{$book->updated_at->diffForHumans()}} @if($book->updatedBy) by <a href="/user/{{ $book->updatedBy->id }}">{{$book->updatedBy->name}}</a> @endif
74 </p> 74 </p>
75 </div> 75 </div>
76 </div> 76 </div>
......
...@@ -49,17 +49,23 @@ ...@@ -49,17 +49,23 @@
49 <hr> 49 <hr>
50 <p class="text-muted">No pages are currently in this chapter.</p> 50 <p class="text-muted">No pages are currently in this chapter.</p>
51 <p> 51 <p>
52 - <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a> 52 + @if(userCan('page-create', $chapter))
53 - &nbsp;&nbsp;<em class="text-muted">-or-</em>&nbsp;&nbsp;&nbsp; 53 + <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a>
54 - <a href="{{$book->getUrl() . '/sort'}}" class="text-book"><i class="zmdi zmdi-book"></i>Sort the current book</a> 54 + @endif
55 + @if(userCan('page-create', $chapter) && userCan('book-update', $book))
56 + &nbsp;&nbsp;<em class="text-muted">-or-</em>&nbsp;&nbsp;&nbsp;
57 + @endif
58 + @if(userCan('book-update', $book))
59 + <a href="{{$book->getUrl() . '/sort'}}" class="text-book"><i class="zmdi zmdi-book"></i>Sort the current book</a>
60 + @endif
55 </p> 61 </p>
56 <hr> 62 <hr>
57 @endif 63 @endif
58 64
59 <p class="text-muted small"> 65 <p class="text-muted small">
60 - Created {{$chapter->created_at->diffForHumans()}} @if($chapter->createdBy) by {{$chapter->createdBy->name}} @endif 66 + Created {{$chapter->created_at->diffForHumans()}} @if($chapter->createdBy) by <a href="/user/{{ $chapter->createdBy->id }}">{{ $chapter->createdBy->name}}</a> @endif
61 <br> 67 <br>
62 - Last Updated {{$chapter->updated_at->diffForHumans()}} @if($chapter->updatedBy) by {{$chapter->updatedBy->name}} @endif 68 + Last Updated {{$chapter->updated_at->diffForHumans()}} @if($chapter->updatedBy) by <a href="/user/{{ $chapter->updatedBy->id }}">{{ $chapter->updatedBy->name}}</a> @endif
63 </p> 69 </p>
64 </div> 70 </div>
65 <div class="col-md-3 col-md-offset-1"> 71 <div class="col-md-3 col-md-offset-1">
......
1 -@extends('base')
2 -
3 -@section('head')
4 - <script src="/libs/tinymce/tinymce.min.js?ver=4.3.7"></script>
5 -@stop
6 -
7 -@section('body-class', 'flexbox')
8 -
9 -@section('content')
10 -
11 - <div class="flex-fill flex">
12 - <form action="{{$book->getUrl() . '/page/' . $draft->id}}" method="POST" class="flex flex-fill">
13 - @include('pages/form', ['model' => $draft])
14 - </form>
15 - </div>
16 - @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $draft->id])
17 -@stop
...\ No newline at end of file ...\ No newline at end of file
...@@ -9,10 +9,15 @@ ...@@ -9,10 +9,15 @@
9 @section('content') 9 @section('content')
10 10
11 <div class="flex-fill flex"> 11 <div class="flex-fill flex">
12 - <form action="{{$page->getUrl()}}" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill"> 12 + <form action="{{$page->getUrl()}}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
13 - <input type="hidden" name="_method" value="PUT"> 13 + @if(!isset($isDraft))
14 + <input type="hidden" name="_method" value="PUT">
15 + @endif
14 @include('pages/form', ['model' => $page]) 16 @include('pages/form', ['model' => $page])
17 + @include('pages/form-toolbox')
15 </form> 18 </form>
19 +
20 +
16 </div> 21 </div>
17 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) 22 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
18 23
......
1 +
2 +<div toolbox class="floating-toolbox">
3 +
4 + <div class="tabs primary-background-light">
5 + <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
6 + <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
7 + </div>
8 +
9 + <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
10 + <h4>Page Tags</h4>
11 + <div class="padded tags">
12 + <p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
13 + <table class="no-style" autosuggestions style="width: 100%;">
14 + <tbody ui-sortable="sortOptions" ng-model="tags" >
15 + <tr ng-repeat="tag in tags track by $index">
16 + <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
17 + <td><input autosuggest="/ajax/tags/suggest/names" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
18 + <td><input autosuggest="/ajax/tags/suggest/values" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
19 + <td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td>
20 + </tr>
21 + </tbody>
22 + </table>
23 + <table class="no-style" style="width: 100%;">
24 + <tbody>
25 + <tr class="unsortable">
26 + <td width="34"></td>
27 + <td ng-click="addEmptyTag()">
28 + <button type="button" class="text-button">Add another tag</button>
29 + </td>
30 + <td></td>
31 + </tr>
32 + </tbody>
33 + </table>
34 + </div>
35 + </div>
36 +
37 +</div>
...\ No newline at end of file ...\ No newline at end of file
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
41 @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title']) 41 @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
42 </div> 42 </div>
43 </div> 43 </div>
44 +
44 <div class="edit-area flex-fill flex"> 45 <div class="edit-area flex-fill flex">
45 @if(setting('app-editor') === 'wysiwyg') 46 @if(setting('app-editor') === 'wysiwyg')
46 <textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" name="html" rows="5" 47 <textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" name="html" rows="5"
......
1 <div ng-non-bindable> 1 <div ng-non-bindable>
2 - <h1 id="bkmrk-page-title">{{$page->name}}</h1> 2 +
3 + <h1 id="bkmrk-page-title" class="float left">{{$page->name}}</h1>
4 +
5 + @if(count($page->tags) > 0)
6 + <div class="tag-display float right">
7 + <div class="heading primary-background-light">Page Tags</div>
8 + <table>
9 + @foreach($page->tags as $tag)
10 + <tr class="tag">
11 + <td @if(!$tag->value) colspan="2" @endif><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%5D">{{ $tag->name }}</a></td>
12 + @if($tag->value) <td class="tag-value"><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%3D{{ urlencode($tag->value) }}%5D">{{$tag->value}}</a></td> @endif
13 + </tr>
14 + @endforeach
15 + </table>
16 + </div>
17 + @endif
18 +
19 + <div style="clear:left;"></div>
3 20
4 {!! $page->html !!} 21 {!! $page->html !!}
5 </div> 22 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -62,9 +62,9 @@ ...@@ -62,9 +62,9 @@
62 <hr> 62 <hr>
63 63
64 <p class="text-muted small"> 64 <p class="text-muted small">
65 - Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif 65 + Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by <a href="/user/{{ $page->createdBy->id }}">{{$page->createdBy->name}}</a> @endif
66 <br> 66 <br>
67 - Last Updated {{$page->updated_at->diffForHumans()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif 67 + Last Updated {{$page->updated_at->diffForHumans()}} @if($page->updatedBy) by <a href="/user/{{ $page->updatedBy->id }}">{{$page->updatedBy->name}}</a> @endif
68 </p> 68 </p>
69 69
70 </div> 70 </div>
......
1 @if(Setting::get('app-color')) 1 @if(Setting::get('app-color'))
2 <style> 2 <style>
3 - header, #back-to-top { 3 + header, #back-to-top, .primary-background {
4 background-color: {{ Setting::get('app-color') }}; 4 background-color: {{ Setting::get('app-color') }};
5 } 5 }
6 - .faded-small { 6 + .faded-small, .primary-background-light {
7 background-color: {{ Setting::get('app-color-light') }}; 7 background-color: {{ Setting::get('app-color-light') }};
8 } 8 }
9 .button-base, .button, input[type="button"], input[type="submit"] { 9 .button-base, .button, input[type="button"], input[type="submit"] {
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
15 .nav-tabs a.selected, .nav-tabs .tab-item.selected { 15 .nav-tabs a.selected, .nav-tabs .tab-item.selected {
16 border-bottom-color: {{ Setting::get('app-color') }}; 16 border-bottom-color: {{ Setting::get('app-color') }};
17 } 17 }
18 - p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus { 18 + p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
19 color: {{ Setting::get('app-color') }}; 19 color: {{ Setting::get('app-color') }};
20 } 20 }
21 </style> 21 </style>
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
25 </div> 25 </div>
26 <div class="form-group"> 26 <div class="form-group">
27 <label>Enable higher security image uploads?</label> 27 <label>Enable higher security image uploads?</label>
28 - <p class="small">For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.</p> 28 + <p class="small">For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.</p>
29 <div toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></div> 29 <div toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></div>
30 </div> 30 </div>
31 <div class="form-group"> 31 <div class="form-group">
...@@ -51,12 +51,16 @@ ...@@ -51,12 +51,16 @@
51 </div> 51 </div>
52 </div> 52 </div>
53 </div> 53 </div>
54 - 54 + <div class="form-group">
55 - 55 + <label for="setting-app-custom-head">Custom HTML head content</label>
56 + <p class="small">Any content added here will be inserted into the bottom of the &lt;head&gt; section of every page. This is handy for overriding styles or adding analytics code.</p>
57 + <textarea name="setting-app-custom-head" id="setting-app-custom-head">{{ setting('app-custom-head', '') }}</textarea>
58 + </div>
56 59
57 <hr class="margin-top"> 60 <hr class="margin-top">
58 61
59 <h3>Registration Settings</h3> 62 <h3>Registration Settings</h3>
63 +
60 <div class="row"> 64 <div class="row">
61 <div class="col-md-6"> 65 <div class="col-md-6">
62 <div class="form-group"> 66 <div class="form-group">
...@@ -66,8 +70,8 @@ ...@@ -66,8 +70,8 @@
66 <div class="form-group"> 70 <div class="form-group">
67 <label for="setting-registration-role">Default user role after registration</label> 71 <label for="setting-registration-role">Default user role after registration</label>
68 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif> 72 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
69 - @foreach(\BookStack\Role::all() as $role) 73 + @foreach(\BookStack\Role::visible() as $role)
70 - <option value="{{$role->id}}" 74 + <option value="{{$role->id}}" data-role-name="{{ $role->name }}"
71 @if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif 75 @if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
72 > 76 >
73 {{ $role->display_name }} 77 {{ $role->display_name }}
......
...@@ -3,9 +3,15 @@ ...@@ -3,9 +3,15 @@
3 <div class="container"> 3 <div class="container">
4 <div class="row"> 4 <div class="row">
5 <div class="col-md-12 setting-nav nav-tabs"> 5 <div class="col-md-12 setting-nav nav-tabs">
6 - <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a> 6 + @if($currentUser->can('settings-manage'))
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" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
8 - <a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a> 8 + @endif
9 + @if($currentUser->can('users-manage'))
10 + <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
11 + @endif
12 + @if($currentUser->can('user-roles-manage'))
13 + <a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
14 + @endif
9 </div> 15 </div>
10 </div> 16 </div>
11 </div> 17 </div>
......
1 <input type="checkbox" name="permissions[{{ $permission }}]" 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 2 + @if(old('permissions'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission)))) checked="checked" @endif
3 value="true"> 3 value="true">
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
18 <label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label> 18 <label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
19 <label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage roles & role permissions</label> 19 <label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage roles & role permissions</label>
20 <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label> 20 <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label>
21 - <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage permissions on own Book, Chapter & Pages</label> 21 + <label>@include('settings/roles/checkbox', ['permission' => 'permissions']) Manage permissions on own Book, Chapter & Pages</label>
22 <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label> 22 <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
23 </div> 23 </div>
24 24
...@@ -31,10 +31,11 @@ ...@@ -31,10 +31,11 @@
31 </p> 31 </p>
32 <table class="table"> 32 <table class="table">
33 <tr> 33 <tr>
34 - <th></th> 34 + <th width="20%"></th>
35 - <th>Create</th> 35 + <th width="20%">Create</th>
36 - <th>Edit</th> 36 + <th width="20%">View</th>
37 - <th>Delete</th> 37 + <th width="20%">Edit</th>
38 + <th width="20%">Delete</th>
38 </tr> 39 </tr>
39 <tr> 40 <tr>
40 <td>Books</td> 41 <td>Books</td>
...@@ -42,6 +43,10 @@ ...@@ -42,6 +43,10 @@
42 <label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label> 43 <label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label>
43 </td> 44 </td>
44 <td> 45 <td>
46 + <label>@include('settings/roles/checkbox', ['permission' => 'book-view-own']) Own</label>
47 + <label>@include('settings/roles/checkbox', ['permission' => 'book-view-all']) All</label>
48 + </td>
49 + <td>
45 <label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label> 50 <label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
46 <label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label> 51 <label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
47 </td> 52 </td>
...@@ -57,6 +62,10 @@ ...@@ -57,6 +62,10 @@
57 <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label> 62 <label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label>
58 </td> 63 </td>
59 <td> 64 <td>
65 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-view-own']) Own</label>
66 + <label>@include('settings/roles/checkbox', ['permission' => 'chapter-view-all']) All</label>
67 + </td>
68 + <td>
60 <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label> 69 <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
61 <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label> 70 <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
62 </td> 71 </td>
...@@ -72,6 +81,10 @@ ...@@ -72,6 +81,10 @@
72 <label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label> 81 <label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label>
73 </td> 82 </td>
74 <td> 83 <td>
84 + <label>@include('settings/roles/checkbox', ['permission' => 'page-view-own']) Own</label>
85 + <label>@include('settings/roles/checkbox', ['permission' => 'page-view-all']) All</label>
86 + </td>
87 + <td>
75 <label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label> 88 <label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
76 <label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label> 89 <label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
77 </td> 90 </td>
...@@ -83,6 +96,7 @@ ...@@ -83,6 +96,7 @@
83 <tr> 96 <tr>
84 <td>Images</td> 97 <td>Images</td>
85 <td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td> 98 <td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
99 + <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
86 <td> 100 <td>
87 <label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label> 101 <label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
88 <label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label> 102 <label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
......
...@@ -6,11 +6,15 @@ ...@@ -6,11 +6,15 @@
6 6
7 <div class="container small"> 7 <div class="container small">
8 8
9 - <h1>User Roles</h1> 9 + <div class="row action-header">
10 - 10 + <div class="col-sm-8">
11 - <p> 11 + <h1>User Roles</h1>
12 - <a href="/settings/roles/new" class="text-pos"><i class="zmdi zmdi-lock-open"></i>Add new role</a> 12 + </div>
13 - </p> 13 + <div class="col-sm-4">
14 + <p></p>
15 + <a href="/settings/roles/new" class="button float right pos"><i class="zmdi zmdi-lock-open"></i>Add new role</a>
16 + </div>
17 + </div>
14 18
15 <table class="table"> 19 <table class="table">
16 <tr> 20 <tr>
......
...@@ -3,33 +3,29 @@ ...@@ -3,33 +3,29 @@
3 3
4 @section('content') 4 @section('content')
5 5
6 - <div class="faded-small toolbar"> 6 + @include('settings/navbar', ['selected' => 'users'])
7 - <div class="container">
8 - <div class="row">
9 - <div class="col-sm-6"></div>
10 - <div class="col-sm-6 faded">
11 - <div class="action-buttons">
12 - <a href="/settings/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
13 - </div>
14 - </div>
15 - </div>
16 - </div>
17 - </div>
18 7
19 8
20 9
21 <div class="container small"> 10 <div class="container small">
22 <form action="/settings/users/{{$user->id}}" method="post"> 11 <form action="/settings/users/{{$user->id}}" method="post">
23 - <div class="row"> 12 + <div class="row">
13 + <div class="col-sm-8">
14 + <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
15 + </div>
16 + <div class="col-sm-4">
17 + <p></p>
18 + <a href="/settings/users/{{$user->id}}/delete" class="neg button float right">Delete User</a>
19 + </div>
20 + </div>
21 + <div class="row">
24 <div class="col-md-6" ng-non-bindable> 22 <div class="col-md-6" ng-non-bindable>
25 - <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
26 {!! csrf_field() !!} 23 {!! csrf_field() !!}
27 <input type="hidden" name="_method" value="put"> 24 <input type="hidden" name="_method" value="put">
28 @include('users.forms.' . $authMethod, ['model' => $user]) 25 @include('users.forms.' . $authMethod, ['model' => $user])
29 26
30 </div> 27 </div>
31 <div class="col-md-6"> 28 <div class="col-md-6">
32 - <h1>&nbsp;</h1>
33 <div class="form-group" id="logo-control"> 29 <div class="form-group" id="logo-control">
34 <label for="user-avatar">User Avatar</label> 30 <label for="user-avatar">User Avatar</label>
35 <p class="small">This image should be approx 256px square.</p> 31 <p class="small">This image should be approx 256px square.</p>
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
13 @if(userCan('users-manage')) 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-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()]) 16 + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
17 </div> 17 </div>
18 @endif 18 @endif
19 19
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
11 @if(userCan('users-manage')) 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-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()]) 14 + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
15 </div> 15 </div>
16 @endif 16 @endif
17 17
......
...@@ -7,17 +7,42 @@ ...@@ -7,17 +7,42 @@
7 7
8 8
9 <div class="container small" ng-non-bindable> 9 <div class="container small" ng-non-bindable>
10 - <h1>Users</h1> 10 + <div class="row action-header">
11 - @if(userCan('users-manage')) 11 + <div class="col-sm-8">
12 - <p> 12 + <h1>Users</h1>
13 - <a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a> 13 + </div>
14 - </p> 14 + <div class="col-sm-4">
15 - @endif 15 + <p></p>
16 + @if(userCan('users-manage'))
17 + <a href="/settings/users/create" class="pos button float right"><i class="zmdi zmdi-account-add"></i>Add new user</a>
18 + @endif
19 + </div>
20 + </div>
21 +
22 + <div class="row">
23 + <div class="col-sm-8">
24 + <div class="compact">
25 + {!! $users->links() !!}
26 + </div>
27 + </div>
28 + <div class="col-sm-4">
29 + <form method="get" class="float right" action="/settings/users">
30 + @foreach(collect($listDetails)->except('search') as $name => $val)
31 + <input type="hidden" name="{{$name}}" value="{{$val}}">
32 + @endforeach
33 + <input type="text" name="search" placeholder="Search Users" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
34 + </form>
35 + </div>
36 + </div>
37 + <div class="text-center">
38 +
39 + </div>
40 +
16 <table class="table"> 41 <table class="table">
17 <tr> 42 <tr>
18 <th></th> 43 <th></th>
19 - <th>Name</th> 44 + <th><a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'name']) }}">Name</a></th>
20 - <th>Email</th> 45 + <th><a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">Email</a></th>
21 <th>User Roles</th> 46 <th>User Roles</th>
22 </tr> 47 </tr>
23 @foreach($users as $user) 48 @foreach($users as $user)
...@@ -42,11 +67,17 @@ ...@@ -42,11 +67,17 @@
42 @endif 67 @endif
43 </td> 68 </td>
44 <td> 69 <td>
45 - <small> {{ $user->roles->implode('display_name', ', ') }}</small> 70 + @foreach($user->roles as $index => $role)
71 + <small><a href="/settings/roles/{{$role->id}}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
72 + @endforeach
46 </td> 73 </td>
47 </tr> 74 </tr>
48 @endforeach 75 @endforeach
49 </table> 76 </table>
77 +
78 + <div>
79 + {!! $users->links() !!}
80 + </div>
50 </div> 81 </div>
51 82
52 @stop 83 @stop
......
...@@ -181,7 +181,7 @@ class AuthTest extends TestCase ...@@ -181,7 +181,7 @@ class AuthTest extends TestCase
181 public function test_user_deletion() 181 public function test_user_deletion()
182 { 182 {
183 $userDetails = factory(\BookStack\User::class)->make(); 183 $userDetails = factory(\BookStack\User::class)->make();
184 - $user = $this->getNewUser($userDetails->toArray()); 184 + $user = $this->getEditor($userDetails->toArray());
185 185
186 $this->asAdmin() 186 $this->asAdmin()
187 ->visit('/settings/users/' . $user->id) 187 ->visit('/settings/users/' . $user->id)
......
...@@ -22,7 +22,7 @@ class LdapTest extends \TestCase ...@@ -22,7 +22,7 @@ class LdapTest extends \TestCase
22 public function test_login() 22 public function test_login()
23 { 23 {
24 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); 24 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
25 - $this->mockLdap->shouldReceive('setOption')->once(); 25 + $this->mockLdap->shouldReceive('setVersion')->once();
26 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4) 26 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
27 ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array')) 27 ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
28 ->andReturn(['count' => 1, 0 => [ 28 ->andReturn(['count' => 1, 0 => [
...@@ -49,7 +49,7 @@ class LdapTest extends \TestCase ...@@ -49,7 +49,7 @@ class LdapTest extends \TestCase
49 public function test_login_works_when_no_uid_provided_by_ldap_server() 49 public function test_login_works_when_no_uid_provided_by_ldap_server()
50 { 50 {
51 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); 51 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
52 - $this->mockLdap->shouldReceive('setOption')->once(); 52 + $this->mockLdap->shouldReceive('setVersion')->once();
53 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn'); 53 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
54 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) 54 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
55 ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array')) 55 ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
...@@ -73,7 +73,7 @@ class LdapTest extends \TestCase ...@@ -73,7 +73,7 @@ class LdapTest extends \TestCase
73 public function test_initial_incorrect_details() 73 public function test_initial_incorrect_details()
74 { 74 {
75 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); 75 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
76 - $this->mockLdap->shouldReceive('setOption')->once(); 76 + $this->mockLdap->shouldReceive('setVersion')->once();
77 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) 77 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
78 ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array')) 78 ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
79 ->andReturn(['count' => 1, 0 => [ 79 ->andReturn(['count' => 1, 0 => [
......
...@@ -161,8 +161,8 @@ class EntityTest extends TestCase ...@@ -161,8 +161,8 @@ class EntityTest extends TestCase
161 public function test_entities_viewable_after_creator_deletion() 161 public function test_entities_viewable_after_creator_deletion()
162 { 162 {
163 // Create required assets and revisions 163 // Create required assets and revisions
164 - $creator = $this->getNewUser(); 164 + $creator = $this->getEditor();
165 - $updater = $this->getNewUser(); 165 + $updater = $this->getEditor();
166 $entities = $this->createEntityChainBelongingToUser($creator, $updater); 166 $entities = $this->createEntityChainBelongingToUser($creator, $updater);
167 $this->actingAs($creator); 167 $this->actingAs($creator);
168 app('BookStack\Repos\UserRepo')->destroy($creator); 168 app('BookStack\Repos\UserRepo')->destroy($creator);
...@@ -174,8 +174,8 @@ class EntityTest extends TestCase ...@@ -174,8 +174,8 @@ class EntityTest extends TestCase
174 public function test_entities_viewable_after_updater_deletion() 174 public function test_entities_viewable_after_updater_deletion()
175 { 175 {
176 // Create required assets and revisions 176 // Create required assets and revisions
177 - $creator = $this->getNewUser(); 177 + $creator = $this->getEditor();
178 - $updater = $this->getNewUser(); 178 + $updater = $this->getEditor();
179 $entities = $this->createEntityChainBelongingToUser($creator, $updater); 179 $entities = $this->createEntityChainBelongingToUser($creator, $updater);
180 $this->actingAs($updater); 180 $this->actingAs($updater);
181 app('BookStack\Repos\UserRepo')->destroy($updater); 181 app('BookStack\Repos\UserRepo')->destroy($updater);
...@@ -198,7 +198,7 @@ class EntityTest extends TestCase ...@@ -198,7 +198,7 @@ class EntityTest extends TestCase
198 198
199 public function test_recently_created_pages_view() 199 public function test_recently_created_pages_view()
200 { 200 {
201 - $user = $this->getNewUser(); 201 + $user = $this->getEditor();
202 $content = $this->createEntityChainBelongingToUser($user); 202 $content = $this->createEntityChainBelongingToUser($user);
203 203
204 $this->asAdmin()->visit('/pages/recently-created') 204 $this->asAdmin()->visit('/pages/recently-created')
...@@ -207,7 +207,7 @@ class EntityTest extends TestCase ...@@ -207,7 +207,7 @@ class EntityTest extends TestCase
207 207
208 public function test_recently_updated_pages_view() 208 public function test_recently_updated_pages_view()
209 { 209 {
210 - $user = $this->getNewUser(); 210 + $user = $this->getEditor();
211 $content = $this->createEntityChainBelongingToUser($user); 211 $content = $this->createEntityChainBelongingToUser($user);
212 212
213 $this->asAdmin()->visit('/pages/recently-updated') 213 $this->asAdmin()->visit('/pages/recently-updated')
...@@ -241,7 +241,7 @@ class EntityTest extends TestCase ...@@ -241,7 +241,7 @@ class EntityTest extends TestCase
241 241
242 public function test_recently_created_pages_on_home() 242 public function test_recently_created_pages_on_home()
243 { 243 {
244 - $entityChain = $this->createEntityChainBelongingToUser($this->getNewUser()); 244 + $entityChain = $this->createEntityChainBelongingToUser($this->getEditor());
245 $this->asAdmin()->visit('/') 245 $this->asAdmin()->visit('/')
246 ->seeInElement('#recently-created-pages', $entityChain['page']->name); 246 ->seeInElement('#recently-created-pages', $entityChain['page']->name);
247 } 247 }
......
...@@ -32,7 +32,7 @@ class PageDraftTest extends TestCase ...@@ -32,7 +32,7 @@ class PageDraftTest extends TestCase
32 ->dontSeeInField('html', $addedContent); 32 ->dontSeeInField('html', $addedContent);
33 33
34 $newContent = $this->page->html . $addedContent; 34 $newContent = $this->page->html . $addedContent;
35 - $newUser = $this->getNewUser(); 35 + $newUser = $this->getEditor();
36 $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); 36 $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
37 $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit') 37 $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
38 ->dontSeeInField('html', $newContent); 38 ->dontSeeInField('html', $newContent);
...@@ -54,7 +54,7 @@ class PageDraftTest extends TestCase ...@@ -54,7 +54,7 @@ class PageDraftTest extends TestCase
54 ->dontSeeInField('html', $addedContent); 54 ->dontSeeInField('html', $addedContent);
55 55
56 $newContent = $this->page->html . $addedContent; 56 $newContent = $this->page->html . $addedContent;
57 - $newUser = $this->getNewUser(); 57 + $newUser = $this->getEditor();
58 $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); 58 $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
59 59
60 $this->actingAs($newUser) 60 $this->actingAs($newUser)
...@@ -79,7 +79,7 @@ class PageDraftTest extends TestCase ...@@ -79,7 +79,7 @@ class PageDraftTest extends TestCase
79 { 79 {
80 $book = \BookStack\Book::first(); 80 $book = \BookStack\Book::first();
81 $chapter = $book->chapters->first(); 81 $chapter = $book->chapters->first();
82 - $newUser = $this->getNewUser(); 82 + $newUser = $this->getEditor();
83 83
84 $this->actingAs($newUser)->visit('/') 84 $this->actingAs($newUser)->visit('/')
85 ->visit($book->getUrl() . '/page/create') 85 ->visit($book->getUrl() . '/page/create')
......
1 +<?php namespace Entity;
2 +
3 +use BookStack\Tag;
4 +use BookStack\Page;
5 +use BookStack\Services\PermissionService;
6 +
7 +class TagTests extends \TestCase
8 +{
9 +
10 + protected $defaultTagCount = 20;
11 +
12 + /**
13 + * Get an instance of a page that has many tags.
14 + * @param Tag[]|bool $tags
15 + * @return mixed
16 + */
17 + protected function getPageWithTags($tags = false)
18 + {
19 + $page = Page::first();
20 +
21 + if (!$tags) {
22 + $tags = factory(Tag::class, $this->defaultTagCount)->make();
23 + }
24 +
25 + $page->tags()->saveMany($tags);
26 + return $page;
27 + }
28 +
29 + public function test_get_page_tags()
30 + {
31 + $page = $this->getPageWithTags();
32 +
33 + // Add some other tags to check they don't interfere
34 + factory(Tag::class, $this->defaultTagCount)->create();
35 +
36 + $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id)
37 + ->shouldReturnJson();
38 +
39 + $json = json_decode($this->response->getContent());
40 + $this->assertTrue(count($json) === $this->defaultTagCount, "Returned JSON item count is not as expected");
41 + }
42 +
43 + public function test_tag_name_suggestions()
44 + {
45 + // Create some tags with similar names to test with
46 + $attrs = collect();
47 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
48 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
49 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'city']));
50 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county']));
51 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet']));
52 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans']));
53 + $page = $this->getPageWithTags($attrs);
54 +
55 + $this->asAdmin()->get('/ajax/tags/suggest/names?search=dog')->seeJsonEquals([]);
56 + $this->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country', 'county']);
57 + $this->get('/ajax/tags/suggest/names?search=cou')->seeJsonEquals(['country', 'county']);
58 + $this->get('/ajax/tags/suggest/names?search=pla')->seeJsonEquals(['planet', 'plans']);
59 + }
60 +
61 + public function test_tag_value_suggestions()
62 + {
63 + // Create some tags with similar values to test with
64 + $attrs = collect();
65 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country', 'value' => 'cats']));
66 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color', 'value' => 'cattery']));
67 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'city', 'value' => 'castle']));
68 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county', 'value' => 'dog']));
69 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet', 'value' => 'catapult']));
70 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans', 'value' => 'dodgy']));
71 + $page = $this->getPageWithTags($attrs);
72 +
73 + $this->asAdmin()->get('/ajax/tags/suggest/values?search=ora')->seeJsonEquals([]);
74 + $this->get('/ajax/tags/suggest/values?search=cat')->seeJsonEquals(['cats', 'cattery', 'catapult']);
75 + $this->get('/ajax/tags/suggest/values?search=do')->seeJsonEquals(['dog', 'dodgy']);
76 + $this->get('/ajax/tags/suggest/values?search=cas')->seeJsonEquals(['castle']);
77 + }
78 +
79 + public function test_entity_permissions_effect_tag_suggestions()
80 + {
81 + $permissionService = $this->app->make(PermissionService::class);
82 +
83 + // Create some tags with similar names to test with and save to a page
84 + $attrs = collect();
85 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
86 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
87 + $page = $this->getPageWithTags($attrs);
88 +
89 + $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
90 + $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
91 +
92 + // Set restricted permission the page
93 + $page->restricted = true;
94 + $page->save();
95 + $permissionService->buildJointPermissionsForEntity($page);
96 +
97 + $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
98 + $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals([]);
99 + }
100 +
101 + public function test_entity_tag_updating()
102 + {
103 + $page = $this->getPageWithTags();
104 +
105 + $testJsonData = [
106 + ['name' => 'color', 'value' => 'red'],
107 + ['name' => 'color', 'value' => ' blue '],
108 + ['name' => 'city', 'value' => 'London '],
109 + ['name' => 'country', 'value' => ' England'],
110 + ];
111 + $testResponseJsonData = [
112 + ['name' => 'color', 'value' => 'red'],
113 + ['name' => 'color', 'value' => 'blue'],
114 + ['name' => 'city', 'value' => 'London'],
115 + ['name' => 'country', 'value' => 'England'],
116 + ];
117 +
118 + // Do update request
119 + $this->asAdmin()->json("POST", "/ajax/tags/update/page/" . $page->id, ['tags' => $testJsonData]);
120 + $updateData = json_decode($this->response->getContent());
121 + // Check data is correct
122 + $testDataCorrect = true;
123 + foreach ($updateData->tags as $data) {
124 + $testItem = ['name' => $data->name, 'value' => $data->value];
125 + if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
126 + }
127 + $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
128 + $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData)));
129 + $this->assertTrue(isset($updateData->message), "No message returned in tag update response");
130 +
131 + // Do get request
132 + $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id);
133 + $getResponseData = json_decode($this->response->getContent());
134 + // Check counts
135 + $this->assertTrue(count($getResponseData) === count($testJsonData), "The received tag count is incorrect");
136 + // Check data is correct
137 + $testDataCorrect = true;
138 + foreach ($getResponseData as $data) {
139 + $testItem = ['name' => $data->name, 'value' => $data->value];
140 + if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
141 + }
142 + $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
143 + $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($getResponseData)));
144 + }
145 +
146 +}
...@@ -4,12 +4,14 @@ class RestrictionsTest extends TestCase ...@@ -4,12 +4,14 @@ class RestrictionsTest extends TestCase
4 { 4 {
5 protected $user; 5 protected $user;
6 protected $viewer; 6 protected $viewer;
7 + protected $restrictionService;
7 8
8 public function setUp() 9 public function setUp()
9 { 10 {
10 parent::setUp(); 11 parent::setUp();
11 - $this->user = $this->getNewUser(); 12 + $this->user = $this->getEditor();
12 $this->viewer = $this->getViewer(); 13 $this->viewer = $this->getViewer();
14 + $this->restrictionService = $this->app[\BookStack\Services\PermissionService::class];
13 } 15 }
14 16
15 protected function getViewer() 17 protected function getViewer()
...@@ -21,28 +23,30 @@ class RestrictionsTest extends TestCase ...@@ -21,28 +23,30 @@ class RestrictionsTest extends TestCase
21 } 23 }
22 24
23 /** 25 /**
24 - * Manually set some restrictions on an entity. 26 + * Manually set some permissions on an entity.
25 * @param \BookStack\Entity $entity 27 * @param \BookStack\Entity $entity
26 * @param $actions 28 * @param $actions
27 */ 29 */
28 protected function setEntityRestrictions(\BookStack\Entity $entity, $actions) 30 protected function setEntityRestrictions(\BookStack\Entity $entity, $actions)
29 { 31 {
30 $entity->restricted = true; 32 $entity->restricted = true;
31 - $entity->restrictions()->delete(); 33 + $entity->permissions()->delete();
32 $role = $this->user->roles->first(); 34 $role = $this->user->roles->first();
33 $viewerRole = $this->viewer->roles->first(); 35 $viewerRole = $this->viewer->roles->first();
34 foreach ($actions as $action) { 36 foreach ($actions as $action) {
35 - $entity->restrictions()->create([ 37 + $entity->permissions()->create([
36 'role_id' => $role->id, 38 'role_id' => $role->id,
37 'action' => strtolower($action) 39 'action' => strtolower($action)
38 ]); 40 ]);
39 - $entity->restrictions()->create([ 41 + $entity->permissions()->create([
40 'role_id' => $viewerRole->id, 42 'role_id' => $viewerRole->id,
41 'action' => strtolower($action) 43 'action' => strtolower($action)
42 ]); 44 ]);
43 } 45 }
44 $entity->save(); 46 $entity->save();
45 - $entity->load('restrictions'); 47 + $entity->load('permissions');
48 + $this->restrictionService->buildJointPermissionsForEntity($entity);
49 + $entity->load('jointPermissions');
46 } 50 }
47 51
48 public function test_book_view_restriction() 52 public function test_book_view_restriction()
...@@ -344,7 +348,7 @@ class RestrictionsTest extends TestCase ...@@ -344,7 +348,7 @@ class RestrictionsTest extends TestCase
344 ->check('restrictions[2][view]') 348 ->check('restrictions[2][view]')
345 ->press('Save Permissions') 349 ->press('Save Permissions')
346 ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true]) 350 ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
347 - ->seeInDatabase('restrictions', [ 351 + ->seeInDatabase('entity_permissions', [
348 'restrictable_id' => $book->id, 352 'restrictable_id' => $book->id,
349 'restrictable_type' => 'BookStack\Book', 353 'restrictable_type' => 'BookStack\Book',
350 'role_id' => '2', 354 'role_id' => '2',
...@@ -361,7 +365,7 @@ class RestrictionsTest extends TestCase ...@@ -361,7 +365,7 @@ class RestrictionsTest extends TestCase
361 ->check('restrictions[2][update]') 365 ->check('restrictions[2][update]')
362 ->press('Save Permissions') 366 ->press('Save Permissions')
363 ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true]) 367 ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
364 - ->seeInDatabase('restrictions', [ 368 + ->seeInDatabase('entity_permissions', [
365 'restrictable_id' => $chapter->id, 369 'restrictable_id' => $chapter->id,
366 'restrictable_type' => 'BookStack\Chapter', 370 'restrictable_type' => 'BookStack\Chapter',
367 'role_id' => '2', 371 'role_id' => '2',
...@@ -378,7 +382,7 @@ class RestrictionsTest extends TestCase ...@@ -378,7 +382,7 @@ class RestrictionsTest extends TestCase
378 ->check('restrictions[2][delete]') 382 ->check('restrictions[2][delete]')
379 ->press('Save Permissions') 383 ->press('Save Permissions')
380 ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true]) 384 ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
381 - ->seeInDatabase('restrictions', [ 385 + ->seeInDatabase('entity_permissions', [
382 'restrictable_id' => $page->id, 386 'restrictable_id' => $page->id,
383 'restrictable_type' => 'BookStack\Page', 387 'restrictable_type' => 'BookStack\Page',
384 'role_id' => '2', 388 'role_id' => '2',
......
...@@ -7,7 +7,15 @@ class RolesTest extends TestCase ...@@ -7,7 +7,15 @@ class RolesTest extends TestCase
7 public function setUp() 7 public function setUp()
8 { 8 {
9 parent::setUp(); 9 parent::setUp();
10 - $this->user = $this->getNewBlankUser(); 10 + $this->user = $this->getViewer();
11 + }
12 +
13 + protected function getViewer()
14 + {
15 + $role = \BookStack\Role::getRole('viewer');
16 + $viewer = $this->getNewBlankUser();
17 + $viewer->attachRole($role);;
18 + return $viewer;
11 } 19 }
12 20
13 /** 21 /**
...@@ -141,7 +149,7 @@ class RolesTest extends TestCase ...@@ -141,7 +149,7 @@ class RolesTest extends TestCase
141 149
142 public function test_restrictions_manage_own_permission() 150 public function test_restrictions_manage_own_permission()
143 { 151 {
144 - $otherUsersPage = \BookStack\Page::take(1)->get()->first(); 152 + $otherUsersPage = \BookStack\Page::first();
145 $content = $this->createEntityChainBelongingToUser($this->user); 153 $content = $this->createEntityChainBelongingToUser($this->user);
146 // Check can't restrict other's content 154 // Check can't restrict other's content
147 $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) 155 $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
...@@ -536,4 +544,27 @@ class RolesTest extends TestCase ...@@ -536,4 +544,27 @@ class RolesTest extends TestCase
536 ->dontSeeInElement('.book-content', $otherPage->name); 544 ->dontSeeInElement('.book-content', $otherPage->name);
537 } 545 }
538 546
547 + public function test_public_role_not_visible_in_user_edit_screen()
548 + {
549 + $user = \BookStack\User::first();
550 + $this->asAdmin()->visit('/settings/users/' . $user->id)
551 + ->seeElement('#roles-admin')
552 + ->dontSeeElement('#roles-public');
553 + }
554 +
555 + public function test_public_role_not_visible_in_role_listing()
556 + {
557 + $this->asAdmin()->visit('/settings/roles')
558 + ->see('Admin')
559 + ->dontSee('Public');
560 + }
561 +
562 + public function test_public_role_not_visible_in_default_role_setting()
563 + {
564 + $this->asAdmin()->visit('/settings')
565 + ->seeElement('[data-role-name="admin"]')
566 + ->dontSeeElement('[data-role-name="public"]');
567 +
568 + }
569 +
539 } 570 }
......
...@@ -14,7 +14,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -14,7 +14,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
14 * @var string 14 * @var string
15 */ 15 */
16 protected $baseUrl = 'http://localhost'; 16 protected $baseUrl = 'http://localhost';
17 +
18 + // Local user instances
17 private $admin; 19 private $admin;
20 + private $editor;
18 21
19 /** 22 /**
20 * Creates the application. 23 * Creates the application.
...@@ -30,6 +33,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -30,6 +33,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
30 return $app; 33 return $app;
31 } 34 }
32 35
36 + /**
37 + * Set the current user context to be an admin.
38 + * @return $this
39 + */
33 public function asAdmin() 40 public function asAdmin()
34 { 41 {
35 if($this->admin === null) { 42 if($this->admin === null) {
...@@ -40,6 +47,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -40,6 +47,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
40 } 47 }
41 48
42 /** 49 /**
50 + * Set the current editor context to be an editor.
51 + * @return $this
52 + */
53 + public function asEditor()
54 + {
55 + if($this->editor === null) {
56 + $this->editor = $this->getEditor();
57 + }
58 + return $this->actingAs($this->editor);
59 + }
60 +
61 + /**
43 * Quickly sets an array of settings. 62 * Quickly sets an array of settings.
44 * @param $settingsArray 63 * @param $settingsArray
45 */ 64 */
...@@ -65,6 +84,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -65,6 +84,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
65 $page = factory(BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]); 84 $page = factory(BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
66 $book->chapters()->saveMany([$chapter]); 85 $book->chapters()->saveMany([$chapter]);
67 $chapter->pages()->saveMany([$page]); 86 $chapter->pages()->saveMany([$page]);
87 + $restrictionService = $this->app[\BookStack\Services\PermissionService::class];
88 + $restrictionService->buildJointPermissionsForEntity($book);
68 return [ 89 return [
69 'book' => $book, 90 'book' => $book,
70 'chapter' => $chapter, 91 'chapter' => $chapter,
...@@ -77,7 +98,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -77,7 +98,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
77 * @param array $attributes 98 * @param array $attributes
78 * @return mixed 99 * @return mixed
79 */ 100 */
80 - protected function getNewUser($attributes = []) 101 + protected function getEditor($attributes = [])
81 { 102 {
82 $user = factory(\BookStack\User::class)->create($attributes); 103 $user = factory(\BookStack\User::class)->create($attributes);
83 $role = \BookStack\Role::getRole('editor'); 104 $role = \BookStack\Role::getRole('editor');
......
...@@ -33,7 +33,7 @@ class UserProfileTest extends TestCase ...@@ -33,7 +33,7 @@ class UserProfileTest extends TestCase
33 33
34 public function test_profile_page_shows_created_content_counts() 34 public function test_profile_page_shows_created_content_counts()
35 { 35 {
36 - $newUser = $this->getNewUser(); 36 + $newUser = $this->getEditor();
37 37
38 $this->asAdmin()->visit('/user/' . $newUser->id) 38 $this->asAdmin()->visit('/user/' . $newUser->id)
39 ->see($newUser->name) 39 ->see($newUser->name)
...@@ -52,7 +52,7 @@ class UserProfileTest extends TestCase ...@@ -52,7 +52,7 @@ class UserProfileTest extends TestCase
52 52
53 public function test_profile_page_shows_recent_activity() 53 public function test_profile_page_shows_recent_activity()
54 { 54 {
55 - $newUser = $this->getNewUser(); 55 + $newUser = $this->getEditor();
56 $this->actingAs($newUser); 56 $this->actingAs($newUser);
57 $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); 57 $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
58 Activity::add($entities['book'], 'book_update', $entities['book']->id); 58 Activity::add($entities['book'], 'book_update', $entities['book']->id);
...@@ -66,7 +66,7 @@ class UserProfileTest extends TestCase ...@@ -66,7 +66,7 @@ class UserProfileTest extends TestCase
66 66
67 public function test_clicking_user_name_in_activity_leads_to_profile_page() 67 public function test_clicking_user_name_in_activity_leads_to_profile_page()
68 { 68 {
69 - $newUser = $this->getNewUser(); 69 + $newUser = $this->getEditor();
70 $this->actingAs($newUser); 70 $this->actingAs($newUser);
71 $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); 71 $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
72 Activity::add($entities['book'], 'book_update', $entities['book']->id); 72 Activity::add($entities['book'], 'book_update', $entities['book']->id);
......