From a54be85185225d6f18f94f041546dd663f8f0644 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 26 Feb 2016 23:44:02 +0000 Subject: [PATCH 01/16] Started work on exposing the role system as editable --- app/Http/Controllers/Controller.php | 1 + app/Http/Controllers/PermissionController.php | 49 ++++++++++++++ app/Http/routes.php | 5 ++ resources/views/settings/navbar.blade.php | 1 + resources/views/settings/roles/edit.blade.php | 64 +++++++++++++++++++ .../views/settings/roles/index.blade.php | 26 ++++++++ 6 files changed, 146 insertions(+) create mode 100644 app/Http/Controllers/PermissionController.php create mode 100644 resources/views/settings/roles/edit.blade.php create mode 100644 resources/views/settings/roles/index.blade.php diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index ab37a44a1..654fed538 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -81,6 +81,7 @@ abstract class Controller extends BaseController protected function checkPermission($permissionName) { if (!$this->currentUser || !$this->currentUser->can($permissionName)) { + dd($this->currentUser); $this->showPermissionError(); } diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php new file mode 100644 index 000000000..69e2619b6 --- /dev/null +++ b/app/Http/Controllers/PermissionController.php @@ -0,0 +1,49 @@ +role = $role; + parent::__construct(); + } + + /** + * Show a listing of the roles in the system. + */ + public function listRoles() + { + $this->checkPermission('settings-update'); + $roles = $this->role->all(); + return view('settings/roles/index', ['roles' => $roles]); + } + + /** + * Show the form for editing a user role. + * @param $id + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function editRole($id) + { + $this->checkPermission('settings-update'); + $role = $this->role->findOrFail($id); + return view('settings/roles/edit', ['role' => $role]); + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 36cf2a19f..eea0a0337 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -87,6 +87,7 @@ Route::group(['middleware' => 'auth'], function () { Route::group(['prefix' => 'settings'], function() { Route::get('/', 'SettingController@index'); Route::post('/', 'SettingController@update'); + // Users Route::get('/users', 'UserController@index'); Route::get('/users/create', 'UserController@create'); @@ -95,6 +96,10 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/users/{id}', 'UserController@edit'); Route::put('/users/{id}', 'UserController@update'); Route::delete('/users/{id}', 'UserController@destroy'); + + // Roles + Route::get('/roles', 'PermissionController@listRoles'); + Route::get('/roles/{id}', 'PermissionController@editRole'); }); }); diff --git a/resources/views/settings/navbar.blade.php b/resources/views/settings/navbar.blade.php index 3afe59a8e..7c3186889 100644 --- a/resources/views/settings/navbar.blade.php +++ b/resources/views/settings/navbar.blade.php @@ -5,6 +5,7 @@
Settings Users + Roles
diff --git a/resources/views/settings/roles/edit.blade.php b/resources/views/settings/roles/edit.blade.php new file mode 100644 index 000000000..ae2d01538 --- /dev/null +++ b/resources/views/settings/roles/edit.blade.php @@ -0,0 +1,64 @@ +@extends('base') + +@section('content') + + @include('settings/navbar', ['selected' => 'roles']) + +
+

Edit Role {{ $role->display_name }}

+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CreateEditDelete
Books
Chapters
Pages
Images
+
+
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+ +@stop diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php new file mode 100644 index 000000000..661d66f63 --- /dev/null +++ b/resources/views/settings/roles/index.blade.php @@ -0,0 +1,26 @@ +@extends('base') + +@section('content') + + @include('settings/navbar', ['selected' => 'roles']) + +
+ +

User Roles

+ + + + + + + @foreach($roles as $role) + + + + + + @endforeach +
Role NameUsers
{{ $role->display_name }}{{ $role->description }}{{ $role->users->count() }}
+
+ +@stop From 473261be35ab50e6c9bc5914c899a34cd6cccf57 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 27 Feb 2016 19:24:42 +0000 Subject: [PATCH 02/16] Finished initial implementation of custom role system --- app/Entity.php | 20 +-- app/Http/Controllers/BookController.php | 24 +-- app/Http/Controllers/ChapterController.php | 21 +-- app/Http/Controllers/Controller.php | 22 ++- app/Http/Controllers/ImageController.php | 8 +- app/Http/Controllers/PageController.php | 27 ++-- app/Http/Controllers/PermissionController.php | 149 +++++++++++++++++- app/Http/Controllers/SettingController.php | 4 +- app/Http/Controllers/UserController.php | 34 ++-- app/Http/routes.php | 5 + app/Image.php | 9 +- app/Ownable.php | 13 +- app/Permission.php | 10 ++ app/Repos/UserRepo.php | 22 +-- app/Role.php | 11 ++ app/User.php | 41 +++-- app/helpers.php | 20 +++ ...27_120329_update_permissions_and_roles.php | 97 ++++++++++++ resources/views/base.blade.php | 2 +- resources/views/books/index.blade.php | 2 +- resources/views/books/show.blade.php | 8 +- resources/views/chapters/show.blade.php | 6 +- .../views/form/role-checkboxes.blade.php | 14 ++ resources/views/pages/show.blade.php | 4 +- resources/views/settings/index.blade.php | 2 +- .../views/settings/roles/checkbox.blade.php | 3 + .../views/settings/roles/create.blade.php | 15 ++ .../views/settings/roles/delete.blade.php | 28 ++++ resources/views/settings/roles/edit.blade.php | 64 ++------ resources/views/settings/roles/form.blade.php | 84 ++++++++++ .../views/settings/roles/index.blade.php | 5 + resources/views/users/forms/ldap.blade.php | 8 +- .../views/users/forms/standard.blade.php | 4 +- resources/views/users/index.blade.php | 16 +- tests/Auth/AuthTest.php | 4 +- tests/RolesTest.php | 48 ++++++ tests/TestCase.php | 3 +- 37 files changed, 644 insertions(+), 213 deletions(-) create mode 100644 database/migrations/2016_02_27_120329_update_permissions_and_roles.php create mode 100644 resources/views/form/role-checkboxes.blade.php create mode 100644 resources/views/settings/roles/checkbox.blade.php create mode 100644 resources/views/settings/roles/create.blade.php create mode 100644 resources/views/settings/roles/delete.blade.php create mode 100644 resources/views/settings/roles/form.blade.php create mode 100644 tests/RolesTest.php diff --git a/app/Entity.php b/app/Entity.php index 42323628a..08aa14638 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -1,14 +1,9 @@ -checkPermission('book-create'); + $this->checkPermission('book-create-all'); $this->setPageTitle('Create New Book'); return view('books/create'); } @@ -68,9 +68,9 @@ class BookController extends Controller */ public function store(Request $request) { - $this->checkPermission('book-create'); + $this->checkPermission('book-create-all'); $this->validate($request, [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000' ]); $book = $this->bookRepo->newFromInput($request->all()); @@ -105,8 +105,8 @@ class BookController extends Controller */ public function edit($slug) { - $this->checkPermission('book-update'); $book = $this->bookRepo->getBySlug($slug); + $this->checkOwnablePermission('book-update', $book); $this->setPageTitle('Edit Book ' . $book->getShortName()); return view('books/edit', ['book' => $book, 'current' => $book]); } @@ -120,10 +120,10 @@ class BookController extends Controller */ public function update(Request $request, $slug) { - $this->checkPermission('book-update'); $book = $this->bookRepo->getBySlug($slug); + $this->checkOwnablePermission('book-update', $book); $this->validate($request, [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000' ]); $book->fill($request->all()); @@ -141,8 +141,8 @@ class BookController extends Controller */ public function showDelete($bookSlug) { - $this->checkPermission('book-delete'); $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-delete', $book); $this->setPageTitle('Delete Book ' . $book->getShortName()); return view('books/delete', ['book' => $book, 'current' => $book]); } @@ -154,8 +154,8 @@ class BookController extends Controller */ public function sort($bookSlug) { - $this->checkPermission('book-update'); $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-update', $book); $bookChildren = $this->bookRepo->getChildren($book); $books = $this->bookRepo->getAll(false); $this->setPageTitle('Sort Book ' . $book->getShortName()); @@ -184,8 +184,8 @@ class BookController extends Controller */ public function saveSort($bookSlug, Request $request) { - $this->checkPermission('book-update'); $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-update', $book); // Return if no map sent if (!$request->has('sort-tree')) { @@ -229,8 +229,8 @@ class BookController extends Controller */ public function destroy($bookSlug) { - $this->checkPermission('book-delete'); $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-delete', $book); Activity::addMessage('book_delete', 0, $book->name); Activity::removeEntity($book); $this->bookRepo->destroyBySlug($bookSlug); diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index fc13e8b58..3b4780f8d 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -1,13 +1,8 @@ -checkPermission('chapter-create'); $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('chapter-create', $book); $this->setPageTitle('Create New Chapter'); return view('chapters/create', ['book' => $book, 'current' => $book]); } @@ -52,12 +46,13 @@ class ChapterController extends Controller */ public function store($bookSlug, Request $request) { - $this->checkPermission('chapter-create'); $this->validate($request, [ 'name' => 'required|string|max:255' ]); $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('chapter-create', $book); + $chapter = $this->chapterRepo->newFromInput($request->all()); $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id); $chapter->priority = $this->bookRepo->getNewPriority($book); @@ -92,9 +87,9 @@ class ChapterController extends Controller */ public function edit($bookSlug, $chapterSlug) { - $this->checkPermission('chapter-update'); $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $this->checkOwnablePermission('chapter-update', $chapter); $this->setPageTitle('Edit Chapter' . $chapter->getShortName()); return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); } @@ -108,9 +103,9 @@ class ChapterController extends Controller */ public function update(Request $request, $bookSlug, $chapterSlug) { - $this->checkPermission('chapter-update'); $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $this->checkOwnablePermission('chapter-update', $chapter); $chapter->fill($request->all()); $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id); $chapter->updated_by = auth()->user()->id; @@ -127,9 +122,9 @@ class ChapterController extends Controller */ public function showDelete($bookSlug, $chapterSlug) { - $this->checkPermission('chapter-delete'); $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $this->checkOwnablePermission('chapter-delete', $chapter); $this->setPageTitle('Delete Chapter' . $chapter->getShortName()); return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); } @@ -142,9 +137,9 @@ class ChapterController extends Controller */ public function destroy($bookSlug, $chapterSlug) { - $this->checkPermission('chapter-delete'); $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $this->checkOwnablePermission('chapter-delete', $chapter); Activity::addMessage('chapter_delete', $book->id, $chapter->name); $this->chapterRepo->destroy($chapter); return redirect($book->getUrl()); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 654fed538..fce479af0 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers; +use BookStack\Ownable; use HttpRequestException; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Exception\HttpResponseException; @@ -61,7 +62,7 @@ abstract class Controller extends BaseController } /** - * On a permission error redirect to home and display + * On a permission error redirect to home and display. * the error as a notification. */ protected function showPermissionError() @@ -74,20 +75,31 @@ abstract class Controller extends BaseController /** * Checks for a permission. - * - * @param $permissionName + * @param string $permissionName * @return bool|\Illuminate\Http\RedirectResponse */ protected function checkPermission($permissionName) { if (!$this->currentUser || !$this->currentUser->can($permissionName)) { - dd($this->currentUser); $this->showPermissionError(); } - return true; } + /** + * Check the current user's permissions against an ownable item. + * @param $permission + * @param Ownable $ownable + * @return bool + */ + protected function checkOwnablePermission($permission, Ownable $ownable) + { + $permissionBaseName = strtolower($permission) . '-'; + if (userCan($permissionBaseName . 'all')) return true; + if (userCan($permissionBaseName . 'own') && $ownable->createdBy->id === $this->currentUser->id) return true; + $this->showPermissionError(); + } + /** * Check if a user has a permission or bypass if the callback is true. * @param $permissionName diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index 3fff28d3b..48e89ee41 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -64,7 +64,7 @@ class ImageController extends Controller */ public function uploadByType($type, Request $request) { - $this->checkPermission('image-create'); + $this->checkPermission('image-create-all'); $this->validate($request, [ 'file' => 'image|mimes:jpeg,gif,png' ]); @@ -90,7 +90,7 @@ class ImageController extends Controller */ public function getThumbnail($id, $width, $height, $crop) { - $this->checkPermission('image-create'); + $this->checkPermission('image-create-all'); $image = $this->imageRepo->getById($id); $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false'); return response()->json(['url' => $thumbnailUrl]); @@ -104,11 +104,11 @@ class ImageController extends Controller */ public function update($imageId, Request $request) { - $this->checkPermission('image-update'); $this->validate($request, [ 'name' => 'required|min:2|string' ]); $image = $this->imageRepo->getById($imageId); + $this->checkOwnablePermission('image-update', $image); $image = $this->imageRepo->updateImageDetails($image, $request->all()); return response()->json($image); } @@ -123,8 +123,8 @@ class ImageController extends Controller */ public function destroy(PageRepo $pageRepo, Request $request, $id) { - $this->checkPermission('image-delete'); $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('image-delete', $image); // Check if this image is used on any pages $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index e78ae13e4..ac968159b 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -1,12 +1,8 @@ -checkPermission('page-create'); $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false; + $parent = $chapter ? $chapter : $book; + $this->checkOwnablePermission('page-create', $parent); $this->setPageTitle('Create New Page'); return view('pages/create', ['book' => $book, 'chapter' => $chapter]); } /** * Store a newly created page in storage. - * * @param Request $request * @param $bookSlug * @return Response */ public function store(Request $request, $bookSlug) { - $this->checkPermission('page-create'); $this->validate($request, [ 'name' => 'required|string|max:255' ]); @@ -72,6 +66,8 @@ class PageController extends Controller $input = $request->all(); $book = $this->bookRepo->getBySlug($bookSlug); $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null; + $parent = $chapterId !== null ? $this->chapterRepo->getById($chapterId) : $book; + $this->checkOwnablePermission('page-create', $parent); $input['priority'] = $this->bookRepo->getNewPriority($book); $page = $this->pageRepo->saveNew($input, $book, $chapterId); @@ -84,7 +80,6 @@ class PageController extends Controller * Display the specified page. * If the page is not found via the slug the * revisions are searched for a match. - * * @param $bookSlug * @param $pageSlug * @return Response @@ -109,23 +104,21 @@ class PageController extends Controller /** * Show the form for editing the specified page. - * * @param $bookSlug * @param $pageSlug * @return Response */ public function edit($bookSlug, $pageSlug) { - $this->checkPermission('page-update'); $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-update', $page); $this->setPageTitle('Editing Page ' . $page->getShortName()); return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); } /** * Update the specified page in storage. - * * @param Request $request * @param $bookSlug * @param $pageSlug @@ -133,12 +126,12 @@ class PageController extends Controller */ public function update(Request $request, $bookSlug, $pageSlug) { - $this->checkPermission('page-update'); $this->validate($request, [ 'name' => 'required|string|max:255' ]); $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-update', $page); $this->pageRepo->updatePage($page, $book->id, $request->all()); Activity::add($page, 'page_update', $book->id); return redirect($page->getUrl()); @@ -164,9 +157,9 @@ class PageController extends Controller */ public function showDelete($bookSlug, $pageSlug) { - $this->checkPermission('page-delete'); $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-delete', $page); $this->setPageTitle('Delete Page ' . $page->getShortName()); return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); } @@ -181,9 +174,9 @@ class PageController extends Controller */ public function destroy($bookSlug, $pageSlug) { - $this->checkPermission('page-delete'); $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-delete', $page); Activity::addMessage('page_delete', $book->id, $page->name); $this->pageRepo->destroy($page); return redirect($book->getUrl()); @@ -229,9 +222,9 @@ class PageController extends Controller */ public function restoreRevision($bookSlug, $pageSlug, $revisionId) { - $this->checkPermission('page-update'); $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-update', $page); $page = $this->pageRepo->restoreRevision($page, $book, $revisionId); Activity::add($page, 'page_restore', $book->id); return redirect($page->getUrl()); diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php index 69e2619b6..8cc14fc7a 100644 --- a/app/Http/Controllers/PermissionController.php +++ b/app/Http/Controllers/PermissionController.php @@ -2,26 +2,27 @@ namespace BookStack\Http\Controllers; +use BookStack\Permission; use BookStack\Role; -use BookStack\User; use Illuminate\Http\Request; - use BookStack\Http\Requests; -use BookStack\Http\Controllers\Controller; class PermissionController extends Controller { protected $role; + protected $permission; /** * PermissionController constructor. - * @param $role - * @param $user + * @param Role $role + * @param Permission $permission + * @internal param $user */ - public function __construct(Role $role) + public function __construct(Role $role, Permission $permission) { $this->role = $role; + $this->permission = $permission; parent::__construct(); } @@ -30,11 +31,54 @@ class PermissionController extends Controller */ public function listRoles() { - $this->checkPermission('settings-update'); + $this->checkPermission('user-roles-manage'); $roles = $this->role->all(); return view('settings/roles/index', ['roles' => $roles]); } + /** + * Show the form to create a new role + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function createRole() + { + $this->checkPermission('user-roles-manage'); + return view('settings/roles/create'); + } + + /** + * Store a new role in the system. + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function storeRole(Request $request) + { + $this->checkPermission('user-roles-manage'); + $this->validate($request, [ + 'display_name' => 'required|min:3|max:200', + 'description' => 'max:250' + ]); + + $role = $this->role->newInstance($request->all()); + $role->name = str_replace(' ', '-', strtolower($request->get('display_name'))); + // Prevent duplicate names + while ($this->role->where('name', '=', $role->name)->count() > 0) { + $role->name .= strtolower(str_random(2)); + } + $role->save(); + + if ($request->has('permissions')) { + $permissionsNames = array_keys($request->get('permissions')); + $permissions = $this->permission->whereIn('name', $permissionsNames)->pluck('id')->toArray(); + $role->permissions()->sync($permissions); + } else { + $role->permissions()->sync([]); + } + + session()->flash('success', 'Role successfully created'); + return redirect('/settings/roles'); + } + /** * Show the form for editing a user role. * @param $id @@ -42,8 +86,97 @@ class PermissionController extends Controller */ public function editRole($id) { - $this->checkPermission('settings-update'); + $this->checkPermission('user-roles-manage'); $role = $this->role->findOrFail($id); return view('settings/roles/edit', ['role' => $role]); } + + /** + * Updates a user role. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function updateRole($id, Request $request) + { + $this->checkPermission('user-roles-manage'); + $this->validate($request, [ + 'display_name' => 'required|min:3|max:200', + 'description' => 'max:250' + ]); + + $role = $this->role->findOrFail($id); + if ($request->has('permissions')) { + $permissionsNames = array_keys($request->get('permissions')); + $permissions = $this->permission->whereIn('name', $permissionsNames)->pluck('id')->toArray(); + $role->permissions()->sync($permissions); + } else { + $role->permissions()->sync([]); + } + + // Ensure admin account always has all permissions + if ($role->name === 'admin') { + $permissions = $this->permission->all()->pluck('id')->toArray(); + $role->permissions()->sync($permissions); + } + + $role->fill($request->all()); + $role->save(); + + session()->flash('success', 'Role successfully updated'); + return redirect('/settings/roles'); + } + + /** + * Show the view to delete a role. + * Offers the chance to migrate users. + * @param $id + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function showDeleteRole($id) + { + $this->checkPermission('user-roles-manage'); + $role = $this->role->findOrFail($id); + $roles = $this->role->where('id', '!=', $id)->get(); + $blankRole = $this->role->newInstance(['display_name' => 'Don\'t migrate users']); + $roles->prepend($blankRole); + return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]); + } + + /** + * Delete a role from the system, + * Migrate from a previous role if set. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function deleteRole($id, Request $request) + { + $this->checkPermission('user-roles-manage'); + $role = $this->role->findOrFail($id); + + // Prevent deleting admin role + if ($role->name === 'admin') { + session()->flash('error', 'The admin role cannot be deleted'); + return redirect()->back(); + } + + if ($role->id == \Setting::get('registration-role')) { + session()->flash('error', 'This role cannot be deleted while set as the default registration role.'); + return redirect()->back(); + } + + if ($request->has('migration_role_id')) { + $newRole = $this->role->find($request->get('migration_role_id')); + if ($newRole) { + $users = $role->users->pluck('id')->toArray(); + $newRole->users()->sync($users); + } + } + + $role->delete(); + + session()->flash('success', 'Role successfully deleted'); + return redirect('/settings/roles'); + } } diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 1739e0b53..c43e6e399 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -17,7 +17,7 @@ class SettingController extends Controller */ public function index() { - $this->checkPermission('settings-update'); + $this->checkPermission('settings-manage'); $this->setPageTitle('Settings'); return view('settings/index'); } @@ -32,7 +32,7 @@ class SettingController extends Controller public function update(Request $request) { $this->preventAccessForDemoUsers(); - $this->checkPermission('settings-update'); + $this->checkPermission('settings-manage'); // Cycles through posted settings and update them foreach($request->all() as $name => $value) { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 55ca5be19..1207c87f1 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -35,7 +35,7 @@ class UserController extends Controller */ public function index() { - $users = $this->user->all(); + $users = $this->userRepo->getAllUsers(); $this->setPageTitle('Users'); return view('users/index', ['users' => $users]); } @@ -46,7 +46,7 @@ class UserController extends Controller */ public function create() { - $this->checkPermission('user-create'); + $this->checkPermission('users-manage'); $authMethod = config('auth.method'); return view('users/create', ['authMethod' => $authMethod]); } @@ -58,11 +58,10 @@ class UserController extends Controller */ public function store(Request $request) { - $this->checkPermission('user-create'); + $this->checkPermission('users-manage'); $validationRules = [ 'name' => 'required', - 'email' => 'required|email|unique:users,email', - 'role' => 'required|exists:roles,id' + 'email' => 'required|email|unique:users,email' ]; $authMethod = config('auth.method'); @@ -84,7 +83,11 @@ class UserController extends Controller } $user->save(); - $user->attachRoleId($request->get('role')); + + if ($request->has('roles')) { + $roles = $request->get('roles'); + $user->roles()->sync($roles); + } // Get avatar from gravatar and save if (!config('services.disable_services')) { @@ -104,7 +107,7 @@ class UserController extends Controller */ public function edit($id, SocialAuthService $socialAuthService) { - $this->checkPermissionOr('user-update', function () use ($id) { + $this->checkPermissionOr('users-manage', function () use ($id) { return $this->currentUser->id == $id; }); @@ -125,7 +128,7 @@ class UserController extends Controller public function update(Request $request, $id) { $this->preventAccessForDemoUsers(); - $this->checkPermissionOr('user-update', function () use ($id) { + $this->checkPermissionOr('users-manage', function () use ($id) { return $this->currentUser->id == $id; }); @@ -133,8 +136,7 @@ class UserController extends Controller 'name' => 'min:2', 'email' => 'min:2|email|unique:users,email,' . $id, 'password' => 'min:5|required_with:password_confirm', - 'password-confirm' => 'same:password|required_with:password', - 'role' => 'exists:roles,id' + 'password-confirm' => 'same:password|required_with:password' ], [ 'password-confirm.required_with' => 'Password confirmation required' ]); @@ -143,8 +145,9 @@ class UserController extends Controller $user->fill($request->all()); // Role updates - if ($this->currentUser->can('user-update') && $request->has('role')) { - $user->attachRoleId($request->get('role')); + if (userCan('users-manage') && $request->has('roles')) { + $roles = $request->get('roles'); + $user->roles()->sync($roles); } // Password updates @@ -154,11 +157,12 @@ class UserController extends Controller } // External auth id updates - if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) { + if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) { $user->external_auth_id = $request->get('external_auth_id'); } $user->save(); + session()->flash('success', 'User successfully updated'); return redirect('/settings/users'); } @@ -169,7 +173,7 @@ class UserController extends Controller */ public function delete($id) { - $this->checkPermissionOr('user-delete', function () use ($id) { + $this->checkPermissionOr('users-manage', function () use ($id) { return $this->currentUser->id == $id; }); @@ -186,7 +190,7 @@ class UserController extends Controller public function destroy($id) { $this->preventAccessForDemoUsers(); - $this->checkPermissionOr('user-delete', function () use ($id) { + $this->checkPermissionOr('users-manage', function () use ($id) { return $this->currentUser->id == $id; }); diff --git a/app/Http/routes.php b/app/Http/routes.php index eea0a0337..a1c737642 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -99,7 +99,12 @@ Route::group(['middleware' => 'auth'], function () { // Roles Route::get('/roles', 'PermissionController@listRoles'); + Route::get('/roles/new', 'PermissionController@createRole'); + Route::post('/roles/new', 'PermissionController@storeRole'); + Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole'); + Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole'); Route::get('/roles/{id}', 'PermissionController@editRole'); + Route::put('/roles/{id}', 'PermissionController@updateRole'); }); }); diff --git a/app/Image.php b/app/Image.php index 3ac084d8f..ad23a077a 100644 --- a/app/Image.php +++ b/app/Image.php @@ -1,14 +1,9 @@ -belongsTo('BookStack\User', 'updated_by'); } + + /** + * Gets the class name. + * @return string + */ + public static function getClassName() + { + return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]); + } + } \ No newline at end of file diff --git a/app/Permission.php b/app/Permission.php index 6859ed56e..794df01ab 100644 --- a/app/Permission.php +++ b/app/Permission.php @@ -13,4 +13,14 @@ class Permission extends Model { return $this->belongsToMany('BookStack\Permissions'); } + + /** + * Get the permission object by name. + * @param $roleName + * @return mixed + */ + public static function getByName($name) + { + return static::where('name', '=', $name)->first(); + } } diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index 48541a51a..15813b3e1 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -42,6 +42,15 @@ class UserRepo return $this->user->findOrFail($id); } + /** + * Get all the users with their permissions. + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function getAllUsers() + { + return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get(); + } + /** * Creates a new user and attaches a role to them. * @param array $data @@ -69,7 +78,7 @@ class UserRepo public function attachDefaultRole($user) { $roleId = Setting::get('registration-role'); - if ($roleId === false) $roleId = $this->role->getDefault()->id; + if ($roleId === false) $roleId = $this->role->first()->id; $user->attachRoleId($roleId); } @@ -80,15 +89,10 @@ class UserRepo */ public function isOnlyAdmin(User $user) { - if ($user->role->name != 'admin') { - return false; - } - - $adminRole = $this->role->where('name', '=', 'admin')->first(); - if (count($adminRole->users) > 1) { - return false; - } + if (!$user->roles->pluck('name')->contains('admin')) return false; + $adminRole = $this->role->getRole('admin'); + if ($adminRole->users->count() > 1) return false; return true; } diff --git a/app/Role.php b/app/Role.php index 3d93bf770..8d5ed7d66 100644 --- a/app/Role.php +++ b/app/Role.php @@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model; class Role extends Model { + + protected $fillable = ['display_name', 'description']; /** * Sets the default role name for newly registered users. * @var string @@ -28,6 +30,15 @@ class Role extends Model return $this->belongsToMany('BookStack\Permission'); } + /** + * Check if this role has a permission. + * @param $permission + */ + public function hasPermission($permission) + { + return $this->permissions->pluck('name')->contains($permission); + } + /** * Add a permission to this role. * @param Permission $permission diff --git a/app/User.php b/app/User.php index c55102078..b062aa78f 100644 --- a/app/User.php +++ b/app/User.php @@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * The database table used by the model. - * * @var string */ protected $table = 'users'; /** * The attributes that are mass assignable. - * * @var array */ protected $fillable = ['name', 'email', 'image_id']; /** * The attributes excluded from the model's JSON form. - * * @var array */ protected $hidden = ['password', 'remember_token']; @@ -50,10 +47,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon ]); } - /** - * Permissions and roles - */ - /** * The roles that belong to the user. */ @@ -62,21 +55,29 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->belongsToMany('BookStack\Role'); } - public function getRoleAttribute() + /** + * Check if the user has a role. + * @param $role + * @return mixed + */ + public function hasRole($role) { - return $this->roles()->with('permissions')->first(); + return $this->roles->pluck('name')->contains($role); } /** - * Loads the user's permissions from their role. + * Get all permissions belonging to a the current user. + * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ - private function loadPermissions() + public function permissions() { - if (isset($this->permissions)) return; + if(isset($this->permissions)) return $this->permissions; $this->load('roles.permissions'); - $permissions = $this->roles[0]->permissions; - $permissionsArray = $permissions->pluck('name')->all(); - $this->permissions = $permissionsArray; + $permissions = $this->roles->map(function($role) { + return $role->permissions; + })->flatten()->unique(); + $this->permissions = $permissions; + return $permissions; } /** @@ -86,11 +87,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function can($permissionName) { - if ($this->email == 'guest') { - return false; - } - $this->loadPermissions(); - return array_search($permissionName, $this->permissions) !== false; + if ($this->email === 'guest') return false; + return $this->permissions()->pluck('name')->contains($permissionName); } /** @@ -113,7 +111,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Get the social account associated with this user. - * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function socialAccounts() @@ -138,8 +135,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Returns the user's avatar, - * Uses Gravatar as the avatar service. - * * @param int $size * @return string */ diff --git a/app/helpers.php b/app/helpers.php index f25a8f765..db65407c7 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -27,4 +27,24 @@ if (! function_exists('versioned_asset')) { throw new InvalidArgumentException("File {$file} not defined in asset manifest."); } +} + +/** + * Check if the current user has a permission. + * If an ownable element is passed in the permissions are checked against + * that particular item. + * @param $permission + * @param \BookStack\Ownable $ownable + * @return mixed + */ +function userCan($permission, \BookStack\Ownable $ownable = null) +{ + if ($ownable === null) { + return auth()->user() && auth()->user()->can($permission); + } + + $permissionBaseName = strtolower($permission) . '-'; + if (userCan($permissionBaseName . 'all')) return true; + if (userCan($permissionBaseName . 'own') && $ownable->createdBy->id === auth()->user()->id) return true; + return false; } \ No newline at end of file diff --git a/database/migrations/2016_02_27_120329_update_permissions_and_roles.php b/database/migrations/2016_02_27_120329_update_permissions_and_roles.php new file mode 100644 index 000000000..9fb2e98f2 --- /dev/null +++ b/database/migrations/2016_02_27_120329_update_permissions_and_roles.php @@ -0,0 +1,97 @@ +each(function ($permission) { + $permission->delete(); + }); + + // Create & attach new admin permissions + $permissionsToCreate = [ + 'settings-manage' => 'Manage Settings', + 'users-manage' => 'Manage Users', + 'user-roles-manage' => 'Manage Roles & Permissions' + ]; + foreach ($permissionsToCreate as $name => $displayName) { + $newPermission = new \BookStack\Permission(); + $newPermission->name = $name; + $newPermission->display_name = $displayName; + $newPermission->save(); + $adminRole->attachPermission($newPermission); + } + + // Create & attach new entity permissions + $entities = ['Book', 'Page', 'Chapter', 'Image']; + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + foreach ($entities as $entity) { + foreach ($ops as $op) { + $newPermission = new \BookStack\Permission(); + $newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + $newPermission->display_name = $op . ' ' . $entity . 's'; + $newPermission->save(); + $adminRole->attachPermission($newPermission); + if ($editorRole !== null) $editorRole->attachPermission($newPermission); + } + } + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // Get roles with permissions we need to change + $adminRole = \BookStack\Role::getRole('admin'); + + // Delete old permissions + $permissions = \BookStack\Permission::all(); + $permissions->each(function ($permission) { + $permission->delete(); + }); + + // Create default CRUD permissions and allocate to admins and editors + $entities = ['Book', 'Page', 'Chapter', 'Image']; + $ops = ['Create', 'Update', 'Delete']; + foreach ($entities as $entity) { + foreach ($ops as $op) { + $newPermission = new \BookStack\Permission(); + $newPermission->name = strtolower($entity) . '-' . strtolower($op); + $newPermission->display_name = $op . ' ' . $entity . 's'; + $newPermission->save(); + $adminRole->attachPermission($newPermission); + } + } + + // Create admin permissions + $entities = ['Settings', 'User']; + $ops = ['Create', 'Update', 'Delete']; + foreach ($entities as $entity) { + foreach ($ops as $op) { + $newPermission = new \BookStack\Permission(); + $newPermission->name = strtolower($entity) . '-' . strtolower($op); + $newPermission->display_name = $op . ' ' . $entity; + $newPermission->save(); + $adminRole->attachPermission($newPermission); + } + } + } +} diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 0df49485e..e3cac3621 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -43,7 +43,7 @@
diff --git a/resources/views/books/restrictions.blade.php b/resources/views/books/restrictions.blade.php index 826f218ce..60b126a7b 100644 --- a/resources/views/books/restrictions.blade.php +++ b/resources/views/books/restrictions.blade.php @@ -2,6 +2,19 @@ @section('content') +
+
+ +
+
+ +

Book Restrictions

@include('form/restriction-form', ['model' => $book]) diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index f8a22ada8..cd32a406b 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -2,7 +2,7 @@ @section('content') -
+
@@ -15,13 +15,22 @@ @endif @if(userCan('book-update', $book)) Edit - Sort @endif - @if(userCan('restrictions-manage', $book)) - Restrict - @endif - @if(userCan('book-delete', $book)) - Delete + @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book)) + @endif
@@ -78,6 +87,15 @@
+ @if($book->restricted) +

+ @if(userCan('restrictions-manage', $book)) + Book Restricted + @else + Book Restricted + @endif +

+ @endif diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index ed0e3dd91..fafb9bed2 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -3,6 +3,7 @@
+

Role Details

@include('form/text', ['name' => 'display_name']) @@ -11,7 +12,7 @@ @include('form/text', ['name' => 'description'])
-
+

System Permissions

@@ -33,10 +34,17 @@
+
+ +

Asset Permissions

+

+ These permissions control default access to the assets within the system.
+ Restrictions on Books, Chapters and Pages will override these permissions. +

@@ -104,4 +112,6 @@ + +Cancel \ No newline at end of file diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php index 601c6533e..8f92a5eba 100644 --- a/resources/views/settings/roles/index.blade.php +++ b/resources/views/settings/roles/index.blade.php @@ -4,7 +4,7 @@ @include('settings/navbar', ['selected' => 'roles']) -
+

User Roles

diff --git a/tests/RestrictionsTest.php b/tests/RestrictionsTest.php new file mode 100644 index 000000000..40b5a7647 --- /dev/null +++ b/tests/RestrictionsTest.php @@ -0,0 +1,407 @@ +user = $this->getNewUser(); + } + + /** + * Manually set some restrictions on an entity. + * @param \BookStack\Entity $entity + * @param $actions + */ + protected function setEntityRestrictions(\BookStack\Entity $entity, $actions) + { + $entity->restricted = true; + $entity->restrictions()->delete(); + $role = $this->user->roles->first(); + foreach ($actions as $action) { + $entity->restrictions()->create([ + 'role_id' => $role->id, + 'action' => strtolower($action) + ]); + } + $entity->save(); + $entity->load('restrictions'); + } + + public function test_book_view_restriction() + { + $book = \BookStack\Book::first(); + $bookPage = $book->pages->first(); + $bookChapter = $book->chapters->first(); + + $bookUrl = $book->getUrl(); + $this->actingAs($this->user) + ->visit($bookUrl) + ->seePageIs($bookUrl); + + $this->setEntityRestrictions($book, []); + + $this->forceVisit($bookUrl) + ->see('Book not found'); + $this->forceVisit($bookPage->getUrl()) + ->see('Book not found'); + $this->forceVisit($bookChapter->getUrl()) + ->see('Book not found'); + + $this->setEntityRestrictions($book, ['view']); + + $this->visit($bookUrl) + ->see($book->name); + $this->visit($bookPage->getUrl()) + ->see($bookPage->name); + $this->visit($bookChapter->getUrl()) + ->see($bookChapter->name); + } + + public function test_book_create_restriction() + { + $book = \BookStack\Book::first(); + + $bookUrl = $book->getUrl(); + $this->actingAs($this->user) + ->visit($bookUrl) + ->seeInElement('.action-buttons', 'New Page') + ->seeInElement('.action-buttons', 'New Chapter'); + + $this->setEntityRestrictions($book, ['view', 'delete', 'update']); + + $this->forceVisit($bookUrl . '/chapter/create') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($bookUrl . '/page/create') + ->see('You do not have permission')->seePageIs('/'); + $this->visit($bookUrl)->dontSeeInElement('.action-buttons', 'New Page') + ->dontSeeInElement('.action-buttons', 'New Chapter'); + + $this->setEntityRestrictions($book, ['view', 'create']); + + $this->visit($bookUrl . '/chapter/create') + ->type('test chapter', 'name') + ->type('test description for chapter', 'description') + ->press('Save Chapter') + ->seePageIs($bookUrl . '/chapter/test-chapter'); + $this->visit($bookUrl . '/page/create') + ->type('test page', 'name') + ->type('test content', 'html') + ->press('Save Page') + ->seePageIs($bookUrl . '/page/test-page'); + $this->visit($bookUrl)->seeInElement('.action-buttons', 'New Page') + ->seeInElement('.action-buttons', 'New Chapter'); + } + + public function test_book_update_restriction() + { + $book = \BookStack\Book::first(); + $bookPage = $book->pages->first(); + $bookChapter = $book->chapters->first(); + + $bookUrl = $book->getUrl(); + $this->actingAs($this->user) + ->visit($bookUrl . '/edit') + ->see('Edit Book'); + + $this->setEntityRestrictions($book, ['view', 'delete']); + + $this->forceVisit($bookUrl . '/edit') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($bookPage->getUrl() . '/edit') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($bookChapter->getUrl() . '/edit') + ->see('You do not have permission')->seePageIs('/'); + + $this->setEntityRestrictions($book, ['view', 'update']); + + $this->visit($bookUrl . '/edit') + ->seePageIs($bookUrl . '/edit'); + $this->visit($bookPage->getUrl() . '/edit') + ->seePageIs($bookPage->getUrl() . '/edit'); + $this->visit($bookChapter->getUrl() . '/edit') + ->see('Edit Chapter'); + } + + public function test_book_delete_restriction() + { + $book = \BookStack\Book::first(); + $bookPage = $book->pages->first(); + $bookChapter = $book->chapters->first(); + + $bookUrl = $book->getUrl(); + $this->actingAs($this->user) + ->visit($bookUrl . '/delete') + ->see('Delete Book'); + + $this->setEntityRestrictions($book, ['view', 'update']); + + $this->forceVisit($bookUrl . '/delete') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($bookPage->getUrl() . '/delete') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($bookChapter->getUrl() . '/delete') + ->see('You do not have permission')->seePageIs('/'); + + $this->setEntityRestrictions($book, ['view', 'delete']); + + $this->visit($bookUrl . '/delete') + ->seePageIs($bookUrl . '/delete')->see('Delete Book'); + $this->visit($bookPage->getUrl() . '/delete') + ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page'); + $this->visit($bookChapter->getUrl() . '/delete') + ->see('Delete Chapter'); + } + + public function test_chapter_view_restriction() + { + $chapter = \BookStack\Chapter::first(); + $chapterPage = $chapter->pages->first(); + + $chapterUrl = $chapter->getUrl(); + $this->actingAs($this->user) + ->visit($chapterUrl) + ->seePageIs($chapterUrl); + + $this->setEntityRestrictions($chapter, []); + + $this->forceVisit($chapterUrl) + ->see('Chapter not found'); + $this->forceVisit($chapterPage->getUrl()) + ->see('Page not found'); + + $this->setEntityRestrictions($chapter, ['view']); + + $this->visit($chapterUrl) + ->see($chapter->name); + $this->visit($chapterPage->getUrl()) + ->see($chapterPage->name); + } + + public function test_chapter_create_restriction() + { + $chapter = \BookStack\Chapter::first(); + + $chapterUrl = $chapter->getUrl(); + $this->actingAs($this->user) + ->visit($chapterUrl) + ->seeInElement('.action-buttons', 'New Page'); + + $this->setEntityRestrictions($chapter, ['view', 'delete', 'update']); + + $this->forceVisit($chapterUrl . '/create-page') + ->see('You do not have permission')->seePageIs('/'); + $this->visit($chapterUrl)->dontSeeInElement('.action-buttons', 'New Page'); + + $this->setEntityRestrictions($chapter, ['view', 'create']); + + + $this->visit($chapterUrl . '/create-page') + ->type('test page', 'name') + ->type('test content', 'html') + ->press('Save Page') + ->seePageIs($chapter->book->getUrl() . '/page/test-page'); + $this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page'); + } + + public function test_chapter_update_restriction() + { + $chapter = \BookStack\Chapter::first(); + $chapterPage = $chapter->pages->first(); + + $chapterUrl = $chapter->getUrl(); + $this->actingAs($this->user) + ->visit($chapterUrl . '/edit') + ->see('Edit Chapter'); + + $this->setEntityRestrictions($chapter, ['view', 'delete']); + + $this->forceVisit($chapterUrl . '/edit') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($chapterPage->getUrl() . '/edit') + ->see('You do not have permission')->seePageIs('/'); + + $this->setEntityRestrictions($chapter, ['view', 'update']); + + $this->visit($chapterUrl . '/edit') + ->seePageIs($chapterUrl . '/edit')->see('Edit Chapter'); + $this->visit($chapterPage->getUrl() . '/edit') + ->seePageIs($chapterPage->getUrl() . '/edit'); + } + + public function test_chapter_delete_restriction() + { + $chapter = \BookStack\Chapter::first(); + $chapterPage = $chapter->pages->first(); + + $chapterUrl = $chapter->getUrl(); + $this->actingAs($this->user) + ->visit($chapterUrl . '/delete') + ->see('Delete Chapter'); + + $this->setEntityRestrictions($chapter, ['view', 'update']); + + $this->forceVisit($chapterUrl . '/delete') + ->see('You do not have permission')->seePageIs('/'); + $this->forceVisit($chapterPage->getUrl() . '/delete') + ->see('You do not have permission')->seePageIs('/'); + + $this->setEntityRestrictions($chapter, ['view', 'delete']); + + $this->visit($chapterUrl . '/delete') + ->seePageIs($chapterUrl . '/delete')->see('Delete Chapter'); + $this->visit($chapterPage->getUrl() . '/delete') + ->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page'); + } + + public function test_page_view_restriction() + { + $page = \BookStack\Page::first(); + + $pageUrl = $page->getUrl(); + $this->actingAs($this->user) + ->visit($pageUrl) + ->seePageIs($pageUrl); + + $this->setEntityRestrictions($page, ['update', 'delete']); + + $this->forceVisit($pageUrl) + ->see('Page not found'); + + $this->setEntityRestrictions($page, ['view']); + + $this->visit($pageUrl) + ->see($page->name); + } + + public function test_page_update_restriction() + { + $page = \BookStack\Chapter::first(); + + $pageUrl = $page->getUrl(); + $this->actingAs($this->user) + ->visit($pageUrl . '/edit') + ->seeInField('name', $page->name); + + $this->setEntityRestrictions($page, ['view', 'delete']); + + $this->forceVisit($pageUrl . '/edit') + ->see('You do not have permission')->seePageIs('/'); + + $this->setEntityRestrictions($page, ['view', 'update']); + + $this->visit($pageUrl . '/edit') + ->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name); + } + + public function test_page_delete_restriction() + { + $page = \BookStack\Page::first(); + + $pageUrl = $page->getUrl(); + $this->actingAs($this->user) + ->visit($pageUrl . '/delete') + ->see('Delete Page'); + + $this->setEntityRestrictions($page, ['view', 'update']); + + $this->forceVisit($pageUrl . '/delete') + ->see('You do not have permission')->seePageIs('/'); + + $this->setEntityRestrictions($page, ['view', 'delete']); + + $this->visit($pageUrl . '/delete') + ->seePageIs($pageUrl . '/delete')->see('Delete Page'); + } + + public function test_book_restriction_form() + { + $book = \BookStack\Book::first(); + $this->asAdmin()->visit($book->getUrl() . '/restrict') + ->see('Book Restrictions') + ->check('restricted') + ->check('restrictions[2][view]') + ->press('Save Restrictions') + ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true]) + ->seeInDatabase('restrictions', [ + 'restrictable_id' => $book->id, + 'restrictable_type' => 'BookStack\Book', + 'role_id' => '2', + 'action' => 'view' + ]); + } + + public function test_chapter_restriction_form() + { + $chapter = \BookStack\Chapter::first(); + $this->asAdmin()->visit($chapter->getUrl() . '/restrict') + ->see('Chapter Restrictions') + ->check('restricted') + ->check('restrictions[2][update]') + ->press('Save Restrictions') + ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true]) + ->seeInDatabase('restrictions', [ + 'restrictable_id' => $chapter->id, + 'restrictable_type' => 'BookStack\Chapter', + 'role_id' => '2', + 'action' => 'update' + ]); + } + + public function test_page_restriction_form() + { + $page = \BookStack\Page::first(); + $this->asAdmin()->visit($page->getUrl() . '/restrict') + ->see('Page Restrictions') + ->check('restricted') + ->check('restrictions[2][delete]') + ->press('Save Restrictions') + ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true]) + ->seeInDatabase('restrictions', [ + 'restrictable_id' => $page->id, + 'restrictable_type' => 'BookStack\Page', + 'role_id' => '2', + 'action' => 'delete' + ]); + } + + public function test_restricted_pages_not_visible_in_book_navigation_on_pages() + { + $chapter = \BookStack\Chapter::first(); + $page = $chapter->pages->first(); + $page2 = $chapter->pages[2]; + + $this->setEntityRestrictions($page, []); + + $this->actingAs($this->user) + ->visit($page2->getUrl()) + ->dontSeeInElement('.sidebar-page-list', $page->name); + } + + public function test_restricted_pages_not_visible_in_book_navigation_on_chapters() + { + $chapter = \BookStack\Chapter::first(); + $page = $chapter->pages->first(); + + $this->setEntityRestrictions($page, []); + + $this->actingAs($this->user) + ->visit($chapter->getUrl()) + ->dontSeeInElement('.sidebar-page-list', $page->name); + } + + public function test_restricted_pages_not_visible_on_chapter_pages() + { + $chapter = \BookStack\Chapter::first(); + $page = $chapter->pages->first(); + + $this->setEntityRestrictions($page, []); + + $this->actingAs($this->user) + ->visit($chapter->getUrl()) + ->dontSee($page->name); + } + +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 840fe0d08..567dc93ec 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,7 @@ assertEquals( + $uri, $this->currentUri, "Did not land on expected page [{$uri}].\n" + ); + + return $this; + } + + /** + * Do a forced visit that does not error out on exception. + * @param string $uri + * @param array $parameters + * @param array $cookies + * @param array $files + * @return $this + */ + protected function forceVisit($uri, $parameters = [], $cookies = [], $files = []) + { + $method = 'GET'; + $uri = $this->prepareUrlForRequest($uri); + $this->call($method, $uri, $parameters, $cookies, $files); + $this->clearInputs()->followRedirects(); + $this->currentUri = $this->app->make('request')->fullUrl(); + $this->crawler = new Crawler($this->response->getContent(), $uri); + return $this; + } + /** * Click the text within the selected element. * @param $parentElement From 76eaf64f945c822824fdc650893cabed8b3ec1d2 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 5 Mar 2016 19:00:26 +0000 Subject: [PATCH 15/16] Fixed errors that occured when merging & refactored entity repositories Also deleted the git '.orig' files that got added in last merge. --- app/Repos/BookRepo.php | 50 +--- app/Repos/BookRepo.php.orig | 295 --------------------- app/Repos/ChapterRepo.php | 54 +--- app/Repos/ChapterRepo.php.orig | 226 ---------------- app/Repos/EntityRepo.php | 83 ++++-- app/Repos/PageRepo.php | 51 +--- app/Repos/PageRepo.php.orig | 437 ------------------------------- app/Services/ActivityService.php | 1 - 8 files changed, 80 insertions(+), 1117 deletions(-) delete mode 100644 app/Repos/BookRepo.php.orig delete mode 100644 app/Repos/ChapterRepo.php.orig delete mode 100644 app/Repos/PageRepo.php.orig diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 816db4cf0..2ec9a4c25 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -1,31 +1,25 @@ book = $book; $this->pageRepo = $pageRepo; $this->chapterRepo = $chapterRepo; - $this->restrictionService = $restrictionService; + parent::__construct(); } /** @@ -90,7 +84,6 @@ class BookRepo */ public function getRecentlyViewed($count = 10, $page = 0) { - // TODO restrict return Views::getUserRecentlyViewed($count, $page, $this->book); } @@ -102,7 +95,6 @@ class BookRepo */ public function getPopular($count = 10, $page = 0) { - // TODO - Restrict return Views::getPopular($count, $page, $this->book); } @@ -241,16 +233,7 @@ class BookRepo */ public function getBySearch($term, $count = 20, $paginationAppends = []) { - preg_match_all('/"(.*?)"/', $term, $matches); - if (count($matches[1]) > 0) { - $terms = $matches[1]; - $term = trim(preg_replace('/"(.*?)"/', '', $term)); - } else { - $terms = []; - } - if (!empty($term)) { - $terms = array_merge($terms, explode(' ', $term)); - } + $terms = $this->prepareSearchTerms($term); $books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) ->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); @@ -262,27 +245,4 @@ class BookRepo return $books; } - /** - * Updates books restrictions from a request - * @param $request - * @param $book - */ - public function updateRestrictionsFromRequest($request, $book) - { - // TODO - extract into shared repo - $book->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $book->restrictions()->delete(); - if ($request->has('restrictions')) { - foreach ($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $book->restrictions()->create([ - 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } - } - $book->save(); - } - } \ No newline at end of file diff --git a/app/Repos/BookRepo.php.orig b/app/Repos/BookRepo.php.orig deleted file mode 100644 index 4b9709fe6..000000000 --- a/app/Repos/BookRepo.php.orig +++ /dev/null @@ -1,295 +0,0 @@ -book = $book; - $this->pageRepo = $pageRepo; - $this->chapterRepo = $chapterRepo; - $this->restrictionService = $restrictionService; - } - - /** - * Base query for getting books. - * Takes into account any restrictions. - * @return mixed - */ - private function bookQuery() - { - return $this->restrictionService->enforceBookRestrictions($this->book, 'view'); - } - - /** - * Get the book that has the given id. - * @param $id - * @return mixed - */ - public function getById($id) - { - return $this->bookQuery()->findOrFail($id); - } - - /** - * Get all books, Limited by count. - * @param int $count - * @return mixed - */ - public function getAll($count = 10) - { - $bookQuery = $this->bookQuery()->orderBy('name', 'asc'); - if (!$count) return $bookQuery->get(); - return $bookQuery->take($count)->get(); - } - - /** - * Get all books paginated. - * @param int $count - * @return mixed - */ - public function getAllPaginated($count = 10) - { - return $this->bookQuery() - ->orderBy('name', 'asc')->paginate($count); - } - - - /** - * Get the latest books. - * @param int $count - * @return mixed - */ - public function getLatest($count = 10) - { - return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get(); - } - - /** - * Gets the most recently viewed for a user. - * @param int $count - * @param int $page - * @return mixed - */ - public function getRecentlyViewed($count = 10, $page = 0) - { - // TODO restrict - return Views::getUserRecentlyViewed($count, $page, $this->book); - } - - /** - * Gets the most viewed books. - * @param int $count - * @param int $page - * @return mixed - */ - public function getPopular($count = 10, $page = 0) - { - // TODO - Restrict - return Views::getPopular($count, $page, $this->book); - } - - /** - * Get a book by slug - * @param $slug - * @return mixed - * @throws NotFoundException - */ - public function getBySlug($slug) - { - $book = $this->bookQuery()->where('slug', '=', $slug)->first(); - if ($book === null) throw new NotFoundException('Book not found'); - return $book; - } - - /** - * Checks if a book exists. - * @param $id - * @return bool - */ - public function exists($id) - { - return $this->bookQuery()->where('id', '=', $id)->exists(); - } - - /** - * Get a new book instance from request input. - * @param $input - * @return Book - */ - public function newFromInput($input) - { - return $this->book->newInstance($input); - } - - /** - * Destroy a book identified by the given slug. - * @param $bookSlug - */ - public function destroyBySlug($bookSlug) - { - $book = $this->getBySlug($bookSlug); - foreach ($book->pages as $page) { - $this->pageRepo->destroy($page); - } - foreach ($book->chapters as $chapter) { - $this->chapterRepo->destroy($chapter); - } - $book->views()->delete(); - $book->restrictions()->delete(); - $book->delete(); - } - - /** - * Get the next child element priority. - * @param Book $book - * @return int - */ - public function getNewPriority($book) - { - $lastElem = $this->getChildren($book)->pop(); - return $lastElem ? $lastElem->priority + 1 : 0; - } - - /** - * @param string $slug - * @param bool|false $currentId - * @return bool - */ - public function doesSlugExist($slug, $currentId = false) - { - $query = $this->book->where('slug', '=', $slug); - if ($currentId) { - $query = $query->where('id', '!=', $currentId); - } - return $query->count() > 0; - } - - /** - * Provides a suitable slug for the given book name. - * Ensures the returned slug is unique in the system. - * @param string $name - * @param bool|false $currentId - * @return string - */ - public function findSuitableSlug($name, $currentId = false) - { - $originalSlug = Str::slug($name); - $slug = $originalSlug; - $count = 2; - while ($this->doesSlugExist($slug, $currentId)) { - $slug = $originalSlug . '-' . $count; - $count++; - } - return $slug; - } - - /** - * Get all child objects of a book. - * Returns a sorted collection of Pages and Chapters. - * Loads the bookslug onto child elements to prevent access database access for getting the slug. - * @param Book $book - * @return mixed - */ - public function getChildren(Book $book) - { - $pageQuery = $book->pages()->where('chapter_id', '=', 0); - $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view'); - $pages = $pageQuery->get(); - - $chapterQuery = $book->chapters()->with(['pages' => function($query) { - $this->restrictionService->enforcePageRestrictions($query, 'view'); - }]); - $chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view'); - $chapters = $chapterQuery->get(); - $children = $pages->merge($chapters); - $bookSlug = $book->slug; - $children->each(function ($child) use ($bookSlug) { - $child->setAttribute('bookSlug', $bookSlug); - if ($child->isA('chapter')) { - $child->pages->each(function ($page) use ($bookSlug) { - $page->setAttribute('bookSlug', $bookSlug); - }); - } - }); - return $children->sortBy('priority'); - } - - /** - * Get books by search term. - * @param $term - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($term, $count = 20, $paginationAppends = []) - { -<<<<<<< HEAD - preg_match_all('/"(.*?)"/', $term, $matches); - if (count($matches[1]) > 0) { - $terms = $matches[1]; - $term = trim(preg_replace('/"(.*?)"/', '', $term)); - } else { - $terms = []; - } - if (!empty($term)) { - $terms = array_merge($terms, explode(' ', $term)); - } - $books = $this->book->fullTextSearchQuery(['name', 'description'], $terms) -======= - $terms = explode(' ', $term); - $books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) ->>>>>>> custom_role_system - ->paginate($count)->appends($paginationAppends); - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - foreach ($books as $book) { - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $book->getExcerpt(100)); - $book->searchSnippet = $result; - } - return $books; - } - - /** - * Updates books restrictions from a request - * @param $request - * @param $book - */ - public function updateRestrictionsFromRequest($request, $book) - { - // TODO - extract into shared repo - $book->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $book->restrictions()->delete(); - if ($request->has('restrictions')) { - foreach ($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $book->restrictions()->create([ - 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } - } - $book->save(); - } - -} \ No newline at end of file diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index 6868bbf89..5d1d6437f 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -3,27 +3,11 @@ use Activity; use BookStack\Exceptions\NotFoundException; -use BookStack\Services\RestrictionService; use Illuminate\Support\Str; use BookStack\Chapter; -class ChapterRepo +class ChapterRepo extends EntityRepo { - - protected $chapter; - protected $restrictionService; - - /** - * ChapterRepo constructor. - * @param Chapter $chapter - * @param RestrictionService $restrictionService - */ - public function __construct(Chapter $chapter, RestrictionService $restrictionService) - { - $this->chapter = $chapter; - $this->restrictionService = $restrictionService; - } - /** * Base query for getting chapters, Takes restrictions into account. * @return mixed @@ -148,7 +132,7 @@ class ChapterRepo /** * Get chapters by the given search term. - * @param $term + * @param string $term * @param array $whereTerms * @param int $count * @param array $paginationAppends @@ -156,16 +140,7 @@ class ChapterRepo */ public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { - preg_match_all('/"(.*?)"/', $term, $matches); - if (count($matches[1]) > 0) { - $terms = $matches[1]; - $term = trim(preg_replace('/"(.*?)"/', '', $term)); - } else { - $terms = []; - } - if (!empty($term)) { - $terms = array_merge($terms, explode(' ', $term)); - } + $terms = $this->prepareSearchTerms($term); $chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) ->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); @@ -195,27 +170,4 @@ class ChapterRepo return $chapter; } - /** - * Updates pages restrictions from a request - * @param $request - * @param $chapter - */ - public function updateRestrictionsFromRequest($request, $chapter) - { - // TODO - extract into shared repo - $chapter->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $chapter->restrictions()->delete(); - if ($request->has('restrictions')) { - foreach($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $chapter->restrictions()->create([ - 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } - } - $chapter->save(); - } - } \ No newline at end of file diff --git a/app/Repos/ChapterRepo.php.orig b/app/Repos/ChapterRepo.php.orig deleted file mode 100644 index be4a4e6b5..000000000 --- a/app/Repos/ChapterRepo.php.orig +++ /dev/null @@ -1,226 +0,0 @@ -chapter = $chapter; - $this->restrictionService = $restrictionService; - } - - /** - * Base query for getting chapters, Takes restrictions into account. - * @return mixed - */ - private function chapterQuery() - { - return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view'); - } - - /** - * Check if an id exists. - * @param $id - * @return bool - */ - public function idExists($id) - { - return $this->chapterQuery()->where('id', '=', $id)->count() > 0; - } - - /** - * Get a chapter by a specific id. - * @param $id - * @return mixed - */ - public function getById($id) - { - return $this->chapterQuery()->findOrFail($id); - } - - /** - * Get all chapters. - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public function getAll() - { - return $this->chapterQuery()->all(); - } - - /** - * Get a chapter that has the given slug within the given book. - * @param $slug - * @param $bookId - * @return mixed - * @throws NotFoundException - */ - public function getBySlug($slug, $bookId) - { - $chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); - if ($chapter === null) throw new NotFoundException('Chapter not found'); - return $chapter; - } - - /** - * Get the child items for a chapter - * @param Chapter $chapter - */ - public function getChildren(Chapter $chapter) - { - return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get(); - } - - /** - * Create a new chapter from request input. - * @param $input - * @return $this - */ - public function newFromInput($input) - { - return $this->chapter->fill($input); - } - - /** - * Destroy a chapter and its relations by providing its slug. - * @param Chapter $chapter - */ - public function destroy(Chapter $chapter) - { - if (count($chapter->pages) > 0) { - foreach ($chapter->pages as $page) { - $page->chapter_id = 0; - $page->save(); - } - } - Activity::removeEntity($chapter); - $chapter->views()->delete(); - $chapter->restrictions()->delete(); - $chapter->delete(); - } - - /** - * Check if a chapter's slug exists. - * @param $slug - * @param $bookId - * @param bool|false $currentId - * @return bool - */ - public function doesSlugExist($slug, $bookId, $currentId = false) - { - $query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId); - if ($currentId) { - $query = $query->where('id', '!=', $currentId); - } - return $query->count() > 0; - } - - /** - * Finds a suitable slug for the provided name. - * Checks database to prevent duplicate slugs. - * @param $name - * @param $bookId - * @param bool|false $currentId - * @return string - */ - public function findSuitableSlug($name, $bookId, $currentId = false) - { - $slug = Str::slug($name); - while ($this->doesSlugExist($slug, $bookId, $currentId)) { - $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); - } - return $slug; - } - - /** - * Get chapters by the given search term. - * @param $term - * @param array $whereTerms - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) - { -<<<<<<< HEAD - preg_match_all('/"(.*?)"/', $term, $matches); - if (count($matches[1]) > 0) { - $terms = $matches[1]; - $term = trim(preg_replace('/"(.*?)"/', '', $term)); - } else { - $terms = []; - } - if (!empty($term)) { - $terms = array_merge($terms, explode(' ', $term)); - } - $chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms) -======= - $terms = explode(' ', $term); - $chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) ->>>>>>> custom_role_system - ->paginate($count)->appends($paginationAppends); - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - foreach ($chapters as $chapter) { - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $chapter->getExcerpt(100)); - $chapter->searchSnippet = $result; - } - return $chapters; - } - - /** - * Changes the book relation of this chapter. - * @param $bookId - * @param Chapter $chapter - * @return Chapter - */ - public function changeBook($bookId, Chapter $chapter) - { - $chapter->book_id = $bookId; - foreach ($chapter->activity as $activity) { - $activity->book_id = $bookId; - $activity->save(); - } - $chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id); - $chapter->save(); - return $chapter; - } - - /** - * Updates pages restrictions from a request - * @param $request - * @param $chapter - */ - public function updateRestrictionsFromRequest($request, $chapter) - { - // TODO - extract into shared repo - $chapter->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $chapter->restrictions()->delete(); - if ($request->has('restrictions')) { - foreach($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $chapter->restrictions()->create([ - 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } - } - $chapter->save(); - } - -} \ No newline at end of file diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 46e1d98a5..9c5184e2f 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -1,32 +1,43 @@ book = $book; - $this->chapter = $chapter; - $this->page = $page; - $this->restrictionService = $restrictionService; + $this->book = app(Book::class); + $this->chapter = app(Chapter::class); + $this->page = app(Page::class); + $this->restrictionService = app(RestrictionService::class); } /** @@ -37,7 +48,7 @@ class EntityRepo public function getRecentlyCreatedBooks($count = 20, $page = 0) { return $this->restrictionService->enforceBookRestrictions($this->book) - ->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get(); + ->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get(); } /** @@ -49,7 +60,7 @@ class EntityRepo public function getRecentlyUpdatedBooks($count = 20, $page = 0) { return $this->restrictionService->enforceBookRestrictions($this->book) - ->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get(); + ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get(); } /** @@ -60,7 +71,7 @@ class EntityRepo public function getRecentlyCreatedPages($count = 20, $page = 0) { return $this->restrictionService->enforcePageRestrictions($this->page) - ->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get(); + ->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get(); } /** @@ -72,7 +83,49 @@ class EntityRepo public function getRecentlyUpdatedPages($count = 20, $page = 0) { return $this->restrictionService->enforcePageRestrictions($this->page) - ->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get(); + ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get(); + } + + /** + * Updates entity restrictions from a request + * @param $request + * @param Entity $entity + */ + public function updateRestrictionsFromRequest($request, Entity $entity) + { + $entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; + $entity->restrictions()->delete(); + if ($request->has('restrictions')) { + foreach ($request->get('restrictions') as $roleId => $restrictions) { + foreach ($restrictions as $action => $value) { + $entity->restrictions()->create([ + 'role_id' => $roleId, + 'action' => strtolower($action) + ]); + } + } + } + $entity->save(); + } + + /** + * Prepare a string of search terms by turning + * it into an array of terms. + * Keeps quoted terms together. + * @param $termString + * @return array + */ + protected function prepareSearchTerms($termString) + { + preg_match_all('/"(.*?)"/', $termString, $matches); + if (count($matches[1]) > 0) { + $terms = $matches[1]; + $termString = trim(preg_replace('/"(.*?)"/', '', $termString)); + } else { + $terms = []; + } + if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString)); + return $terms; } diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 3d675183e..4784ad407 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -3,34 +3,23 @@ use Activity; use BookStack\Book; -use BookStack\Chapter; use BookStack\Exceptions\NotFoundException; -use BookStack\Services\RestrictionService; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use BookStack\Page; use BookStack\PageRevision; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -class PageRepo +class PageRepo extends EntityRepo { - protected $page; protected $pageRevision; - protected $restrictionService; /** * PageRepo constructor. - * @param Page $page * @param PageRevision $pageRevision - * @param RestrictionService $restrictionService */ - public function __construct(Page $page, PageRevision $pageRevision, RestrictionService $restrictionService) + public function __construct(PageRevision $pageRevision) { - $this->page = $page; $this->pageRevision = $pageRevision; - $this->restrictionService = $restrictionService; + parent::__construct(); } /** @@ -200,16 +189,7 @@ class PageRepo */ public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { - preg_match_all('/"(.*?)"/', $term, $matches); - if (count($matches[1]) > 0) { - $terms = $matches[1]; - $term = trim(preg_replace('/"(.*?)"/', '', $term)); - } else { - $terms = []; - } - if (!empty($term)) { - $terms = array_merge($terms, explode(' ', $term)); - } + $terms = $this->prepareSearchTerms($term); $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) ->paginate($count)->appends($paginationAppends); @@ -416,27 +396,4 @@ class PageRepo return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count); } - /** - * Updates pages restrictions from a request - * @param $request - * @param $page - */ - public function updateRestrictionsFromRequest($request, $page) - { - // TODO - extract into shared repo - $page->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $page->restrictions()->delete(); - if ($request->has('restrictions')) { - foreach($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $page->restrictions()->create([ - 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } - } - $page->save(); - } - } diff --git a/app/Repos/PageRepo.php.orig b/app/Repos/PageRepo.php.orig deleted file mode 100644 index c1e02b501..000000000 --- a/app/Repos/PageRepo.php.orig +++ /dev/null @@ -1,437 +0,0 @@ -page = $page; - $this->pageRevision = $pageRevision; - $this->restrictionService = $restrictionService; - } - - /** - * Base query for getting pages, Takes restrictions into account. - * @return mixed - */ - private function pageQuery() - { - return $this->restrictionService->enforcePageRestrictions($this->page, 'view'); - } - - /** - * Get a page via a specific ID. - * @param $id - * @return mixed - */ - public function getById($id) - { - return $this->pageQuery()->findOrFail($id); - } - - /** - * Get a page identified by the given slug. - * @param $slug - * @param $bookId - * @return mixed - * @throws NotFoundException - */ - public function getBySlug($slug, $bookId) - { - $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); - if ($page === null) throw new NotFoundException('Page not found'); - return $page; - } - - /** - * Search through page revisions and retrieve - * the last page in the current book that - * has a slug equal to the one given. - * @param $pageSlug - * @param $bookSlug - * @return null | Page - */ - public function findPageUsingOldSlug($pageSlug, $bookSlug) - { - $revision = $this->pageRevision->where('slug', '=', $pageSlug) - ->whereHas('page', function($query) { - $this->restrictionService->enforcePageRestrictions($query); - }) - ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') - ->with('page')->first(); - return $revision !== null ? $revision->page : null; - } - - /** - * Get a new Page instance from the given input. - * @param $input - * @return Page - */ - public function newFromInput($input) - { - $page = $this->page->fill($input); - return $page; - } - - - /** - * Save a new page into the system. - * Input validation must be done beforehand. - * @param array $input - * @param Book $book - * @param int $chapterId - * @return Page - */ - public function saveNew(array $input, Book $book, $chapterId = null) - { - $page = $this->newFromInput($input); - $page->slug = $this->findSuitableSlug($page->name, $book->id); - - if ($chapterId) $page->chapter_id = $chapterId; - - $page->html = $this->formatHtml($input['html']); - $page->text = strip_tags($page->html); - $page->created_by = auth()->user()->id; - $page->updated_by = auth()->user()->id; - - $book->pages()->save($page); - return $page; - } - - /** - * Formats a page's html to be tagged correctly - * within the system. - * @param string $htmlText - * @return string - */ - protected function formatHtml($htmlText) - { - if($htmlText == '') return $htmlText; - libxml_use_internal_errors(true); - $doc = new \DOMDocument(); - $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); - - $container = $doc->documentElement; - $body = $container->childNodes->item(0); - $childNodes = $body->childNodes; - - // Ensure no duplicate ids are used - $idArray = []; - - foreach ($childNodes as $index => $childNode) { - /** @var \DOMElement $childNode */ - if (get_class($childNode) !== 'DOMElement') continue; - - // Overwrite id if not a BookStack custom id - if ($childNode->hasAttribute('id')) { - $id = $childNode->getAttribute('id'); - if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { - $idArray[] = $id; - continue; - }; - } - - // Create an unique id for the element - // Uses the content as a basis to ensure output is the same every time - // the same content is passed through. - $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20); - $newId = urlencode($contentId); - $loopIndex = 0; - while (in_array($newId, $idArray)) { - $newId = urlencode($contentId . '-' . $loopIndex); - $loopIndex++; - } - - $childNode->setAttribute('id', $newId); - $idArray[] = $newId; - } - - // Generate inner html as a string - $html = ''; - foreach ($childNodes as $childNode) { - $html .= $doc->saveHTML($childNode); - } - - return $html; - } - - - /** - * Gets pages by a search term. - * Highlights page content for showing in results. - * @param string $term - * @param array $whereTerms - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) - { -<<<<<<< HEAD - preg_match_all('/"(.*?)"/', $term, $matches); - if (count($matches[1]) > 0) { - $terms = $matches[1]; - $term = trim(preg_replace('/"(.*?)"/', '', $term)); - } else { - $terms = []; - } - if (!empty($term)) { - $terms = array_merge($terms, explode(' ', $term)); - } - $pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms) -======= - $terms = explode(' ', $term); - $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) ->>>>>>> custom_role_system - ->paginate($count)->appends($paginationAppends); - - // Add highlights to page text. - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - //lookahead/behind assertions ensures cut between words - $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words - - foreach ($pages as $page) { - preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER); - //delimiter between occurrences - $results = []; - foreach ($matches as $line) { - $results[] = htmlspecialchars($line[0], 0, 'UTF-8'); - } - $matchLimit = 6; - if (count($results) > $matchLimit) { - $results = array_slice($results, 0, $matchLimit); - } - $result = join('... ', $results); - - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $result); - if (strlen($result) < 5) { - $result = $page->getExcerpt(80); - } - $page->searchSnippet = $result; - } - return $pages; - } - - /** - * Search for image usage. - * @param $imageString - * @return mixed - */ - public function searchForImage($imageString) - { - $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get(); - foreach ($pages as $page) { - $page->url = $page->getUrl(); - $page->html = ''; - $page->text = ''; - } - return count($pages) > 0 ? $pages : false; - } - - /** - * Updates a page with any fillable data and saves it into the database. - * @param Page $page - * @param int $book_id - * @param string $input - * @return Page - */ - public function updatePage(Page $page, $book_id, $input) - { - // Save a revision before updating - if ($page->html !== $input['html'] || $page->name !== $input['name']) { - $this->saveRevision($page); - } - - // Prevent slug being updated if no name change - if ($page->name !== $input['name']) { - $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id); - } - - // Update with new details - $page->fill($input); - $page->html = $this->formatHtml($input['html']); - $page->text = strip_tags($page->html); - $page->updated_by = auth()->user()->id; - $page->save(); - return $page; - } - - /** - * Restores a revision's content back into a page. - * @param Page $page - * @param Book $book - * @param int $revisionId - * @return Page - */ - public function restoreRevision(Page $page, Book $book, $revisionId) - { - $this->saveRevision($page); - $revision = $this->getRevisionById($revisionId); - $page->fill($revision->toArray()); - $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id); - $page->text = strip_tags($page->html); - $page->updated_by = auth()->user()->id; - $page->save(); - return $page; - } - - /** - * Saves a page revision into the system. - * @param Page $page - * @return $this - */ - public function saveRevision(Page $page) - { - $revision = $this->pageRevision->fill($page->toArray()); - $revision->page_id = $page->id; - $revision->slug = $page->slug; - $revision->book_slug = $page->book->slug; - $revision->created_by = auth()->user()->id; - $revision->created_at = $page->updated_at; - $revision->save(); - // Clear old revisions - if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { - $this->pageRevision->where('page_id', '=', $page->id) - ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); - } - return $revision; - } - - /** - * Gets a single revision via it's id. - * @param $id - * @return mixed - */ - public function getRevisionById($id) - { - return $this->pageRevision->findOrFail($id); - } - - /** - * Checks if a slug exists within a book already. - * @param $slug - * @param $bookId - * @param bool|false $currentId - * @return bool - */ - public function doesSlugExist($slug, $bookId, $currentId = false) - { - $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId); - if ($currentId) $query = $query->where('id', '!=', $currentId); - return $query->count() > 0; - } - - /** - * Changes the related book for the specified page. - * Changes the book id of any relations to the page that store the book id. - * @param int $bookId - * @param Page $page - * @return Page - */ - public function changeBook($bookId, Page $page) - { - $page->book_id = $bookId; - foreach ($page->activity as $activity) { - $activity->book_id = $bookId; - $activity->save(); - } - $page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id); - $page->save(); - return $page; - } - - /** - * Gets a suitable slug for the resource - * @param $name - * @param $bookId - * @param bool|false $currentId - * @return string - */ - public function findSuitableSlug($name, $bookId, $currentId = false) - { - $slug = Str::slug($name); - while ($this->doesSlugExist($slug, $bookId, $currentId)) { - $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); - } - return $slug; - } - - /** - * Destroy a given page along with its dependencies. - * @param $page - */ - public function destroy($page) - { - Activity::removeEntity($page); - $page->views()->delete(); - $page->revisions()->delete(); - $page->restrictions()->delete(); - $page->delete(); - } - - /** - * Get the latest pages added to the system. - * @param $count - */ - public function getRecentlyCreatedPaginated($count = 20) - { - return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count); - } - - /** - * Get the latest pages added to the system. - * @param $count - */ - public function getRecentlyUpdatedPaginated($count = 20) - { - return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count); - } - - /** - * Updates pages restrictions from a request - * @param $request - * @param $page - */ - public function updateRestrictionsFromRequest($request, $page) - { - // TODO - extract into shared repo - $page->restricted = $request->has('restricted') && $request->get('restricted') === 'true'; - $page->restrictions()->delete(); - if ($request->has('restrictions')) { - foreach($request->get('restrictions') as $roleId => $restrictions) { - foreach ($restrictions as $action => $value) { - $page->restrictions()->create([ - 'role_id' => $roleId, - 'action' => strtolower($action) - ]); - } - } - } - $page->save(); - } - -} diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index a57210b8d..118bd6d9c 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -1,6 +1,5 @@ Date: Sat, 5 Mar 2016 22:54:53 +0000 Subject: [PATCH 16/16] Fixed incorrect recents pages on homescreen Fixed the bug causing the recently updated pages to be exaclty the same as the recently create pages. Also added in tests to prevent regression. --- app/Http/Controllers/HomeController.php | 1 - resources/views/home.blade.php | 8 ++++++-- tests/EntityTest.php | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index e20c89e06..489396df6 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -24,7 +24,6 @@ class HomeController extends Controller /** * Display the homepage. - * * @return Response */ public function index() diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 8aaae1e1b..f840be965 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -33,10 +33,14 @@

Recently Created Pages

- @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) +
+ @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) +

Recently Updated Pages

- @include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) +
+ @include('partials/entity-list', ['entities' => $recentlyUpdatedPages, 'style' => 'compact']) +
diff --git a/tests/EntityTest.php b/tests/EntityTest.php index 2936fc047..30858f8d9 100644 --- a/tests/EntityTest.php +++ b/tests/EntityTest.php @@ -225,4 +225,22 @@ class EntityTest extends TestCase ->seePageIs($newPageUrl); } + public function test_recently_updated_pages_on_home() + { + $page = \BookStack\Page::orderBy('updated_at', 'asc')->first(); + $this->asAdmin()->visit('/') + ->dontSeeInElement('#recently-updated-pages', $page->name); + $this->visit($page->getUrl() . '/edit') + ->press('Save Page') + ->visit('/') + ->seeInElement('#recently-updated-pages', $page->name); + } + + public function test_recently_created_pages_on_home() + { + $entityChain = $this->createEntityChainBelongingToUser($this->getNewUser()); + $this->asAdmin()->visit('/') + ->seeInElement('#recently-created-pages', $entityChain['page']->name); + } + }