From a54be85185225d6f18f94f041546dd663f8f0644 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 26 Feb 2016 23:44:02 +0000 Subject: [PATCH 01/20] 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/20] 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 @@
@@ -89,3 +94,7 @@ @include('partials/image-manager', ['imageType' => 'system']) @stop + +@section('scripts') + +@stop \ No newline at end of file From 33bf20cfc8e0903da0df4f17481d2a47a9a3ddb5 Mon Sep 17 00:00:00 2001 From: Nick Walke Date: Fri, 4 Mar 2016 02:33:57 -0600 Subject: [PATCH 11/20] Found the source of the issue, not sure how to fix --- app/Entity.php | 13 ++++++++----- app/Repos/PageRepo.php | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/Entity.php b/app/Entity.php index 42323628a..122aa2aa2 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -100,10 +100,14 @@ abstract class Entity extends Model */ public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) { - $termString = ''; - foreach ($terms as $term) { - $termString .= htmlentities($term) . '* '; + foreach ($terms as $key => $term) { + $term = htmlentities($term); + if (preg_match('/\s/', $term)) { + $term = '"' . $term . '"'; + } + $terms[$key] = $term . '*'; } + $termString = "'" . implode(' ', $terms) . "'"; $fields = implode(',', $fieldsToSearch); $termStringEscaped = \DB::connection()->getPdo()->quote($termString); $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance')); @@ -113,9 +117,8 @@ abstract class Entity extends Model foreach ($wheres as $whereTerm) { $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); } - // Load in relations - if (static::isA('page')) { + if (static::isA('page')) { $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); } else if (static::isA('chapter')) { $search = $search->with('book'); diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index fa5f7dd01..bfaff79ae 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -208,7 +208,9 @@ class PageRepo } else { $terms = []; } - $terms = array_merge($terms, explode(' ', $term)); + if (!empty($term)) { + $terms = array_merge($terms, explode(' ', $term)); + } $pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms) ->paginate($count)->appends($paginationAppends); From bc2b310638d7769e3e0f6c168e0c0618d5f245ec Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 4 Mar 2016 20:49:35 +0000 Subject: [PATCH 12/20] Updated fulltext search with custom escaped query Changed pdo quoted query to use custom escaping and added like searches when quoted terms are used. --- app/Entity.php | 29 ++++++++++++++++++++++------- app/Repos/BookRepo.php | 4 +++- app/Repos/ChapterRepo.php | 4 +++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/Entity.php b/app/Entity.php index 122aa2aa2..4e85e252e 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -87,8 +87,8 @@ abstract class Entity extends Model */ public function getShortName($length = 25) { - if(strlen($this->name) <= $length) return $this->name; - return substr($this->name, 0, $length-3) . '...'; + if (strlen($this->name) <= $length) return $this->name; + return substr($this->name, 0, $length - 3) . '...'; } /** @@ -100,19 +100,34 @@ abstract class Entity extends Model */ public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) { + $exactTerms = []; foreach ($terms as $key => $term) { - $term = htmlentities($term); + $term = htmlentities($term, ENT_QUOTES); + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); if (preg_match('/\s/', $term)) { + $exactTerms[] = '%' . $term . '%'; $term = '"' . $term . '"'; + } else { + $term = '' . $term . '*'; } - $terms[$key] = $term . '*'; + if ($term !== '*') $terms[$key] = $term; } - $termString = "'" . implode(' ', $terms) . "'"; + $termString = implode(' ', $terms); $fields = implode(',', $fieldsToSearch); - $termStringEscaped = \DB::connection()->getPdo()->quote($termString); - $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance')); + $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); + // Ensure at least one exact term matches if in search + if (count($exactTerms) > 0) { + $search = $search->where(function($query) use ($exactTerms, $fieldsToSearch) { + foreach ($exactTerms as $exactTerm) { + foreach ($fieldsToSearch as $field) { + $query->orWhere($field, 'like', $exactTerm); + } + } + }); + } + // Add additional where terms foreach ($wheres as $whereTerm) { $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 4b45a1a0b..afd802337 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -233,7 +233,9 @@ class BookRepo } else { $terms = []; } - $terms = array_merge($terms, explode(' ', $term)); + if (!empty($term)) { + $terms = array_merge($terms, explode(' ', $term)); + } $books = $this->book->fullTextSearchQuery(['name', 'description'], $terms) ->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index a3ed0a837..542cdd532 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -138,7 +138,9 @@ class ChapterRepo } else { $terms = []; } - $terms = array_merge($terms, explode(' ', $term)); + if (!empty($term)) { + $terms = array_merge($terms, explode(' ', $term)); + } $chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms) ->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); From 479dd80a8c0a971e5fc5db8679f3677205e44109 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 5 Mar 2016 09:47:24 +0000 Subject: [PATCH 13/20] Made memcached config allow mulitple servers --- .env.example | 13 +++++++------ config/cache.php | 21 ++++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 9d42d7487..5661cda22 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,12 @@ SESSION_DRIVER=file #SESSION_DRIVER=memcached QUEUE_DRIVER=sync +# Memcached settings +# If using a UNIX socket path for the host, set the port to 0 +# This follows the following format: HOST:PORT:WEIGHT +# For multiple servers separate with a comma +MEMCACHED_SERVERS=127.0.0.1:11211:100 + # Storage STORAGE_TYPE=local # Amazon S3 Config @@ -56,9 +62,4 @@ MAIL_HOST=localhost MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null -MAIL_ENCRYPTION=null - -# Memcached settings -#MEMCACHED_HOST=127.0.0.1 -# If using a UNIX socket path for the host, set the port to 0 -#MEMCACHED_PORT=11211 \ No newline at end of file +MAIL_ENCRYPTION=null \ No newline at end of file diff --git a/config/cache.php b/config/cache.php index b00a9989e..076a0299f 100644 --- a/config/cache.php +++ b/config/cache.php @@ -1,5 +1,18 @@ $memcachedServer) { + $memcachedServerDetails = explode(':', $memcachedServer); + $components = count($memcachedServerDetails); + if ($components < 2) $memcachedServerDetails[] = '11211'; + if ($components < 3) $memcachedServerDetails[] = '100'; + $memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails); + } +} + return [ /* @@ -49,13 +62,7 @@ return [ 'memcached' => [ 'driver' => 'memcached', - 'servers' => [ - [ - 'host' => env('MEMCACHED_HOST', '127.0.0.1'), - 'port' => env('MEMCACHED_PORT', 11211), - 'weight' => 100, - ], - ], + 'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [], ], 'redis' => [ From 268db6b1d0409766014ae9f1681ec1bf5bab7552 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 5 Mar 2016 12:09:09 +0000 Subject: [PATCH 14/20] Added a whole load of permission & role tests --- app/Activity.php | 2 +- app/Http/Controllers/UserController.php | 1 + app/Repos/BookRepo.php | 2 +- app/Repos/PermissionsRepo.php | 1 + app/User.php | 7 +- .../views/partials/activity-item.blade.php | 2 +- resources/views/settings/roles/form.blade.php | 2 +- tests/RolesTest.php | 431 +++++++++++++++++- tests/TestCase.php | 11 + 9 files changed, 450 insertions(+), 9 deletions(-) diff --git a/app/Activity.php b/app/Activity.php index a1fe608f0..ac7c1d749 100644 --- a/app/Activity.php +++ b/app/Activity.php @@ -15,10 +15,10 @@ class Activity extends Model /** * Get the entity for this activity. - * @return bool */ public function entity() { + if ($this->entity_type === '') $this->entity_type = null; return $this->morphTo('entity'); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 1207c87f1..9f6a4105f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -35,6 +35,7 @@ class UserController extends Controller */ public function index() { + $this->checkPermission('users-manage'); $users = $this->userRepo->getAllUsers(); $this->setPageTitle('Users'); return view('users/index', ['users' => $users]); diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 572030d43..73572f25e 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -136,7 +136,7 @@ class BookRepo */ public function newFromInput($input) { - return $this->bookQuery()->fill($input); + return $this->book->newInstance($input); } /** diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index c35f29d10..2d497b76a 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -101,6 +101,7 @@ class PermissionsRepo public function assignRolePermissions(Role $role, $permissionNameArray = []) { $permissions = []; + $permissionNameArray = array_values($permissionNameArray); if ($permissionNameArray && count($permissionNameArray) > 0) { $permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray(); } diff --git a/app/User.php b/app/User.php index 2d14c6e6e..e1b7c143b 100644 --- a/app/User.php +++ b/app/User.php @@ -67,11 +67,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Get all permissions belonging to a the current user. + * @param bool $cache * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ - public function permissions() + public function permissions($cache = true) { - if(isset($this->permissions)) return $this->permissions; + if(isset($this->permissions) && $cache) return $this->permissions; $this->load('roles.permissions'); $permissions = $this->roles->map(function($role) { return $role->permissions; @@ -106,7 +107,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function attachRoleId($id) { - $this->roles()->attach([$id]); + $this->roles()->attach($id); } /** diff --git a/resources/views/partials/activity-item.blade.php b/resources/views/partials/activity-item.blade.php index 00ca574dd..ff0d74586 100644 --- a/resources/views/partials/activity-item.blade.php +++ b/resources/views/partials/activity-item.blade.php @@ -16,7 +16,7 @@ {{ $activity->getText() }} - @if($activity->entity()) + @if($activity->entity) {{ $activity->entity->name }} @endif diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 0758f317a..ed0e3dd91 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -17,7 +17,7 @@
- +

diff --git a/tests/RolesTest.php b/tests/RolesTest.php index 7349c2968..baba208f1 100644 --- a/tests/RolesTest.php +++ b/tests/RolesTest.php @@ -7,16 +7,33 @@ class RolesTest extends TestCase public function setUp() { parent::setUp(); + $this->user = $this->getNewBlankUser(); + } + + /** + * Give the given user some permissions. + * @param \BookStack\User $user + * @param array $permissions + */ + protected function giveUserPermissions(\BookStack\User $user, $permissions = []) + { + $newRole = $this->createNewRole($permissions); + $user->attachRole($newRole); + $user->load('roles'); + $user->permissions(false); } /** * Create a new basic role for testing purposes. + * @param array $permissions * @return static */ - protected function createNewRole() + protected function createNewRole($permissions = []) { $permissionRepo = app('BookStack\Repos\PermissionsRepo'); - return $permissionRepo->saveNewRole(factory(\BookStack\Role::class)->make()->toArray()); + $roleData = factory(\BookStack\Role::class)->make()->toArray(); + $roleData['permissions'] = array_flip($permissions); + return $permissionRepo->saveNewRole($roleData); } public function test_admin_can_see_settings() @@ -80,4 +97,414 @@ class RolesTest extends TestCase ->dontSee($testRoleUpdateName); } + public function test_manage_user_permission() + { + $this->actingAs($this->user)->visit('/')->visit('/settings/users') + ->seePageIs('/'); + $this->giveUserPermissions($this->user, ['users-manage']); + $this->actingAs($this->user)->visit('/')->visit('/settings/users') + ->seePageIs('/settings/users'); + } + + public function test_user_roles_manage_permission() + { + $this->actingAs($this->user)->visit('/')->visit('/settings/roles') + ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/'); + $this->giveUserPermissions($this->user, ['user-roles-manage']); + $this->actingAs($this->user)->visit('/settings/roles') + ->seePageIs('/settings/roles')->click('Admin') + ->see('Edit Role'); + } + + public function test_settings_manage_permission() + { + $this->actingAs($this->user)->visit('/')->visit('/settings') + ->seePageIs('/'); + $this->giveUserPermissions($this->user, ['settings-manage']); + $this->actingAs($this->user)->visit('/')->visit('/settings') + ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved'); + } + + public function test_restrictions_manage_all_permission() + { + $page = \BookStack\Page::take(1)->get()->first(); + $this->actingAs($this->user)->visit($page->getUrl()) + ->dontSee('Restrict') + ->visit($page->getUrl() . '/restrict') + ->seePageIs('/'); + $this->giveUserPermissions($this->user, ['restrictions-manage-all']); + $this->actingAs($this->user)->visit($page->getUrl()) + ->see('Restrict') + ->click('Restrict') + ->see('Page Restrictions')->seePageIs($page->getUrl() . '/restrict'); + } + + public function test_restrictions_manage_own_permission() + { + $otherUsersPage = \BookStack\Page::take(1)->get()->first(); + $content = $this->createEntityChainBelongingToUser($this->user); + // Check can't restrict other's content + $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) + ->dontSee('Restrict') + ->visit($otherUsersPage->getUrl() . '/restrict') + ->seePageIs('/'); + // Check can't restrict own content + $this->actingAs($this->user)->visit($content['page']->getUrl()) + ->dontSee('Restrict') + ->visit($content['page']->getUrl() . '/restrict') + ->seePageIs('/'); + + $this->giveUserPermissions($this->user, ['restrictions-manage-own']); + + // Check can't restrict other's content + $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) + ->dontSee('Restrict') + ->visit($otherUsersPage->getUrl() . '/restrict') + ->seePageIs('/'); + // Check can restrict own content + $this->actingAs($this->user)->visit($content['page']->getUrl()) + ->see('Restrict') + ->click('Restrict') + ->seePageIs($content['page']->getUrl() . '/restrict'); + } + + /** + * Check a standard entity access permission + * @param string $permission + * @param array $accessUrls Urls that are only accessible after having the permission + * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission + * @param null $callback + */ + private function checkAccessPermission($permission, $accessUrls = [], $visibles = []) + { + foreach ($accessUrls as $url) { + $this->actingAs($this->user)->visit('/')->visit($url) + ->seePageIs('/'); + } + foreach ($visibles as $url => $text) { + $this->actingAs($this->user)->visit('/')->visit($url) + ->dontSeeInElement('.action-buttons',$text); + } + + $this->giveUserPermissions($this->user, [$permission]); + + foreach ($accessUrls as $url) { + $this->actingAs($this->user)->visit('/')->visit($url) + ->seePageIs($url); + } + foreach ($visibles as $url => $text) { + $this->actingAs($this->user)->visit('/')->visit($url) + ->see($text); + } + } + + public function test_books_create_all_permissions() + { + $this->checkAccessPermission('book-create-all', [ + '/books/create' + ], [ + '/books' => 'Add new book' + ]); + + $this->visit('/books/create') + ->type('test book', 'name') + ->type('book desc', 'description') + ->press('Save Book') + ->seePageIs('/books/test-book'); + } + + public function test_books_edit_own_permission() + { + $otherBook = \BookStack\Book::take(1)->get()->first(); + $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; + $this->checkAccessPermission('book-update-own', [ + $ownBook->getUrl() . '/edit' + ], [ + $ownBook->getUrl() => 'Edit' + ]); + + $this->visit($otherBook->getUrl()) + ->dontSeeInElement('.action-buttons', 'Edit') + ->visit($otherBook->getUrl() . '/edit') + ->seePageIs('/'); + } + + public function test_books_edit_all_permission() + { + $otherBook = \BookStack\Book::take(1)->get()->first(); + $this->checkAccessPermission('book-update-all', [ + $otherBook->getUrl() . '/edit' + ], [ + $otherBook->getUrl() => 'Edit' + ]); + } + + public function test_books_delete_own_permission() + { + $this->giveUserPermissions($this->user, ['book-update-all']); + $otherBook = \BookStack\Book::take(1)->get()->first(); + $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; + $this->checkAccessPermission('book-delete-own', [ + $ownBook->getUrl() . '/delete' + ], [ + $ownBook->getUrl() => 'Delete' + ]); + + $this->visit($otherBook->getUrl()) + ->dontSeeInElement('.action-buttons', 'Delete') + ->visit($otherBook->getUrl() . '/delete') + ->seePageIs('/'); + $this->visit($ownBook->getUrl())->visit($ownBook->getUrl() . '/delete') + ->press('Confirm') + ->seePageIs('/books') + ->dontSee($ownBook->name); + } + + public function test_books_delete_all_permission() + { + $this->giveUserPermissions($this->user, ['book-update-all']); + $otherBook = \BookStack\Book::take(1)->get()->first(); + $this->checkAccessPermission('book-delete-all', [ + $otherBook->getUrl() . '/delete' + ], [ + $otherBook->getUrl() => 'Delete' + ]); + + $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete') + ->press('Confirm') + ->seePageIs('/books') + ->dontSee($otherBook->name); + } + + public function test_chapter_create_own_permissions() + { + $book = \BookStack\Book::take(1)->get()->first(); + $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; + $baseUrl = $ownBook->getUrl() . '/chapter'; + $this->checkAccessPermission('chapter-create-own', [ + $baseUrl . '/create' + ], [ + $ownBook->getUrl() => 'New Chapter' + ]); + + $this->visit($baseUrl . '/create') + ->type('test chapter', 'name') + ->type('chapter desc', 'description') + ->press('Save Chapter') + ->seePageIs($baseUrl . '/test-chapter'); + + $this->visit($book->getUrl()) + ->dontSeeInElement('.action-buttons', 'New Chapter') + ->visit($book->getUrl() . '/chapter/create') + ->seePageIs('/'); + } + + public function test_chapter_create_all_permissions() + { + $book = \BookStack\Book::take(1)->get()->first(); + $baseUrl = $book->getUrl() . '/chapter'; + $this->checkAccessPermission('chapter-create-all', [ + $baseUrl . '/create' + ], [ + $book->getUrl() => 'New Chapter' + ]); + + $this->visit($baseUrl . '/create') + ->type('test chapter', 'name') + ->type('chapter desc', 'description') + ->press('Save Chapter') + ->seePageIs($baseUrl . '/test-chapter'); + } + + public function test_chapter_edit_own_permission() + { + $otherChapter = \BookStack\Chapter::take(1)->get()->first(); + $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; + $this->checkAccessPermission('chapter-update-own', [ + $ownChapter->getUrl() . '/edit' + ], [ + $ownChapter->getUrl() => 'Edit' + ]); + + $this->visit($otherChapter->getUrl()) + ->dontSeeInElement('.action-buttons', 'Edit') + ->visit($otherChapter->getUrl() . '/edit') + ->seePageIs('/'); + } + + public function test_chapter_edit_all_permission() + { + $otherChapter = \BookStack\Chapter::take(1)->get()->first(); + $this->checkAccessPermission('chapter-update-all', [ + $otherChapter->getUrl() . '/edit' + ], [ + $otherChapter->getUrl() => 'Edit' + ]); + } + + public function test_chapter_delete_own_permission() + { + $this->giveUserPermissions($this->user, ['chapter-update-all']); + $otherChapter = \BookStack\Chapter::take(1)->get()->first(); + $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; + $this->checkAccessPermission('chapter-delete-own', [ + $ownChapter->getUrl() . '/delete' + ], [ + $ownChapter->getUrl() => 'Delete' + ]); + + $bookUrl = $ownChapter->book->getUrl(); + $this->visit($otherChapter->getUrl()) + ->dontSeeInElement('.action-buttons', 'Delete') + ->visit($otherChapter->getUrl() . '/delete') + ->seePageIs('/'); + $this->visit($ownChapter->getUrl())->visit($ownChapter->getUrl() . '/delete') + ->press('Confirm') + ->seePageIs($bookUrl) + ->dontSeeInElement('.book-content', $ownChapter->name); + } + + public function test_chapter_delete_all_permission() + { + $this->giveUserPermissions($this->user, ['chapter-update-all']); + $otherChapter = \BookStack\Chapter::take(1)->get()->first(); + $this->checkAccessPermission('chapter-delete-all', [ + $otherChapter->getUrl() . '/delete' + ], [ + $otherChapter->getUrl() => 'Delete' + ]); + + $bookUrl = $otherChapter->book->getUrl(); + $this->visit($otherChapter->getUrl())->visit($otherChapter->getUrl() . '/delete') + ->press('Confirm') + ->seePageIs($bookUrl) + ->dontSeeInElement('.book-content', $otherChapter->name); + } + + public function test_page_create_own_permissions() + { + $book = \BookStack\Book::take(1)->get()->first(); + $chapter = \BookStack\Chapter::take(1)->get()->first(); + + $entities = $this->createEntityChainBelongingToUser($this->user); + $ownBook = $entities['book']; + $ownChapter = $entities['chapter']; + + $baseUrl = $ownBook->getUrl() . '/page'; + + $this->checkAccessPermission('page-create-own', [ + $baseUrl . '/create', + $ownChapter->getUrl() . '/create-page' + ], [ + $ownBook->getUrl() => 'New Page', + $ownChapter->getUrl() => 'New Page' + ]); + + $this->visit($baseUrl . '/create') + ->type('test page', 'name') + ->type('page desc', 'html') + ->press('Save Page') + ->seePageIs($baseUrl . '/test-page'); + + $this->visit($book->getUrl()) + ->dontSeeInElement('.action-buttons', 'New Page') + ->visit($book->getUrl() . '/page/create') + ->seePageIs('/'); + $this->visit($chapter->getUrl()) + ->dontSeeInElement('.action-buttons', 'New Page') + ->visit($chapter->getUrl() . '/create-page') + ->seePageIs('/'); + } + + public function test_page_create_all_permissions() + { + $book = \BookStack\Book::take(1)->get()->first(); + $chapter = \BookStack\Chapter::take(1)->get()->first(); + $baseUrl = $book->getUrl() . '/page'; + $this->checkAccessPermission('page-create-all', [ + $baseUrl . '/create', + $chapter->getUrl() . '/create-page' + ], [ + $book->getUrl() => 'New Page', + $chapter->getUrl() => 'New Page' + ]); + + $this->visit($baseUrl . '/create') + ->type('test page', 'name') + ->type('page desc', 'html') + ->press('Save Page') + ->seePageIs($baseUrl . '/test-page'); + + $this->visit($chapter->getUrl() . '/create-page') + ->type('new test page', 'name') + ->type('page desc', 'html') + ->press('Save Page') + ->seePageIs($baseUrl . '/new-test-page'); + } + + public function test_page_edit_own_permission() + { + $otherPage = \BookStack\Page::take(1)->get()->first(); + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $this->checkAccessPermission('page-update-own', [ + $ownPage->getUrl() . '/edit' + ], [ + $ownPage->getUrl() => 'Edit' + ]); + + $this->visit($otherPage->getUrl()) + ->dontSeeInElement('.action-buttons', 'Edit') + ->visit($otherPage->getUrl() . '/edit') + ->seePageIs('/'); + } + + public function test_page_edit_all_permission() + { + $otherPage = \BookStack\Page::take(1)->get()->first(); + $this->checkAccessPermission('page-update-all', [ + $otherPage->getUrl() . '/edit' + ], [ + $otherPage->getUrl() => 'Edit' + ]); + } + + public function test_page_delete_own_permission() + { + $this->giveUserPermissions($this->user, ['page-update-all']); + $otherPage = \BookStack\Page::take(1)->get()->first(); + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $this->checkAccessPermission('page-delete-own', [ + $ownPage->getUrl() . '/delete' + ], [ + $ownPage->getUrl() => 'Delete' + ]); + + $bookUrl = $ownPage->book->getUrl(); + $this->visit($otherPage->getUrl()) + ->dontSeeInElement('.action-buttons', 'Delete') + ->visit($otherPage->getUrl() . '/delete') + ->seePageIs('/'); + $this->visit($ownPage->getUrl())->visit($ownPage->getUrl() . '/delete') + ->press('Confirm') + ->seePageIs($bookUrl) + ->dontSeeInElement('.book-content', $ownPage->name); + } + + public function test_page_delete_all_permission() + { + $this->giveUserPermissions($this->user, ['page-update-all']); + $otherPage = \BookStack\Page::take(1)->get()->first(); + $this->checkAccessPermission('page-delete-all', [ + $otherPage->getUrl() . '/delete' + ], [ + $otherPage->getUrl() => 'Delete' + ]); + + $bookUrl = $otherPage->book->getUrl(); + $this->visit($otherPage->getUrl())->visit($otherPage->getUrl() . '/delete') + ->press('Confirm') + ->seePageIs($bookUrl) + ->dontSeeInElement('.book-content', $otherPage->name); + } + } diff --git a/tests/TestCase.php b/tests/TestCase.php index a521fd076..840fe0d08 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -84,6 +84,17 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase return $user; } + /** + * Quick way to create a new user without any permissions + * @param array $attributes + * @return mixed + */ + protected function getNewBlankUser($attributes = []) + { + $user = factory(\BookStack\User::class)->create($attributes); + return $user; + } + /** * Assert that a given string is seen inside an element. * From 8e6248f57f92d943a011c3219120d60ee4f2f00b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 5 Mar 2016 18:09:21 +0000 Subject: [PATCH 15/20] Added restriction tests and fixed any bugs in the process Also updated many styles within areas affected by the new permission and roles system. --- app/Exceptions/Handler.php | 3 +- app/Exceptions/NotFoundException.php | 14 + app/Http/Controllers/ChapterController.php | 9 +- app/Http/Controllers/PageController.php | 3 +- app/Repos/BookRepo.php | 13 +- app/Repos/ChapterRepo.php | 14 +- app/Repos/PageRepo.php | 5 +- app/Services/RestrictionService.php | 30 +- app/helpers.php | 9 +- database/seeds/DummyContentSeeder.php | 2 +- phpunit.xml | 1 + resources/assets/sass/_header.scss | 3 + resources/assets/sass/_lists.scss | 3 +- resources/views/books/index.blade.php | 4 +- resources/views/books/restrictions.blade.php | 13 + resources/views/books/show.blade.php | 32 +- .../views/chapters/restrictions.blade.php | 14 + resources/views/chapters/show.blade.php | 27 +- resources/views/errors/404.blade.php | 2 +- .../views/form/restriction-form.blade.php | 3 +- resources/views/pages/restrictions.blade.php | 21 + resources/views/pages/show.blade.php | 31 ++ resources/views/settings/roles/form.blade.php | 12 +- .../views/settings/roles/index.blade.php | 2 +- tests/RestrictionsTest.php | 407 ++++++++++++++++++ tests/TestCase.php | 35 ++ 26 files changed, 680 insertions(+), 32 deletions(-) create mode 100644 app/Exceptions/NotFoundException.php create mode 100644 tests/RestrictionsTest.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 73a316953..14d553ed0 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -56,7 +56,8 @@ class Handler extends ExceptionHandler // Which will include the basic message to point the user roughly to the cause. if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) { $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); - return response()->view('errors/500', ['message' => $message], 500); + $code = ($e->getCode() === 0) ? 500 : $e->getCode(); + return response()->view('errors/' . $code, ['message' => $message], $code); } return parent::render($request, $e); diff --git a/app/Exceptions/NotFoundException.php b/app/Exceptions/NotFoundException.php new file mode 100644 index 000000000..3c027dc44 --- /dev/null +++ b/app/Exceptions/NotFoundException.php @@ -0,0 +1,14 @@ +bookRepo->getChildren($book); Views::add($chapter); $this->setPageTitle($chapter->getShortName()); - return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]); + $pages = $this->chapterRepo->getChildren($chapter); + return view('chapters/show', [ + 'book' => $book, + 'chapter' => $chapter, + 'current' => $chapter, + 'sidebarTree' => $sidebarTree, + 'pages' => $pages + ]); } /** diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index b469f51dd..19e4744ea 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -1,6 +1,7 @@ pageRepo->getBySlug($pageSlug, $book->id); - } catch (NotFoundHttpException $e) { + } catch (NotFoundException $e) { $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug); if ($page === null) abort(404); return redirect($page->getUrl()); diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 73572f25e..4ae7cc062 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -1,6 +1,7 @@ bookQuery()->where('slug', '=', $slug)->first(); - if ($book === null) abort(404); + if ($book === null) throw new NotFoundException('Book not found'); return $book; } @@ -153,6 +155,7 @@ class BookRepo $this->chapterRepo->destroy($chapter); } $book->views()->delete(); + $book->restrictions()->delete(); $book->delete(); } @@ -210,11 +213,13 @@ class BookRepo public function getChildren(Book $book) { $pageQuery = $book->pages()->where('chapter_id', '=', 0); - $this->restrictionService->enforcePageRestrictions($pageQuery, 'view'); + $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view'); $pages = $pageQuery->get(); - $chapterQuery = $book->chapters()->with('pages'); - $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view'); + $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; diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index 90f2f8c54..095596a60 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -2,6 +2,7 @@ use Activity; +use BookStack\Exceptions\NotFoundException; use BookStack\Services\RestrictionService; use Illuminate\Support\Str; use BookStack\Chapter; @@ -66,14 +67,24 @@ class ChapterRepo * @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) abort(404); + 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 @@ -98,6 +109,7 @@ class ChapterRepo } Activity::removeEntity($chapter); $chapter->views()->delete(); + $chapter->restrictions()->delete(); $chapter->delete(); } diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index c4cf00e7c..f3933af69 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -4,6 +4,7 @@ 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; @@ -56,11 +57,12 @@ class PageRepo * @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 NotFoundHttpException('Page not found'); + if ($page === null) throw new NotFoundException('Page not found'); return $page; } @@ -373,6 +375,7 @@ class PageRepo Activity::removeEntity($page); $page->views()->delete(); $page->revisions()->delete(); + $page->restrictions()->delete(); $page->delete(); } diff --git a/app/Services/RestrictionService.php b/app/Services/RestrictionService.php index 0ef80b229..f7838bf88 100644 --- a/app/Services/RestrictionService.php +++ b/app/Services/RestrictionService.php @@ -15,10 +15,16 @@ class RestrictionService public function __construct() { $user = auth()->user(); - $this->userRoles = $user ? auth()->user()->roles->pluck('id') : false; + $this->userRoles = $user ? auth()->user()->roles->pluck('id') : []; $this->isAdmin = $user ? auth()->user()->hasRole('admin') : false; } + /** + * Checks if an entity has a restriction set upon it. + * @param Entity $entity + * @param $action + * @return bool + */ public function checkIfEntityRestricted(Entity $entity, $action) { if ($this->isAdmin) return true; @@ -93,12 +99,28 @@ class RestrictionService }); }); }) + // Page unrestricted, Has an unrestricted chapter & book has accepted restrictions + ->orWhere(function ($query) { + $query->where('restricted', '=', false) + ->whereExists(function ($query) { + $query->select('*')->from('chapters') + ->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false); + }) + ->whereExists(function ($query) { + $query->select('*')->from('books') + ->whereRaw('books.id=pages.book_id') + ->whereExists(function ($query) { + $this->checkRestrictionsQuery($query, 'books', 'Book'); + }); + }); + }) // Page unrestricted, Has a chapter with accepted permissions ->orWhere(function ($query) { $query->where('restricted', '=', false) ->whereExists(function ($query) { $query->select('*')->from('chapters') ->whereRaw('chapters.id=pages.chapter_id') + ->where('restricted', '=', true) ->whereExists(function ($query) { $this->checkRestrictionsQuery($query, 'chapters', 'Chapter'); }); @@ -183,8 +205,10 @@ class RestrictionService return $query->where(function ($parentWhereQuery) { $parentWhereQuery ->where('restricted', '=', false) - ->orWhereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); + ->orWhere(function ($query) { + $query->where('restricted', '=', true)->whereExists(function ($query) { + $this->checkRestrictionsQuery($query, 'books', 'Book'); + }); }); }); } diff --git a/app/helpers.php b/app/helpers.php index 8f080c5e1..ead6b3008 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,10 +1,10 @@ check()) return false; if ($ownable === null) { return auth()->user() && auth()->user()->can($permission); } @@ -47,9 +48,9 @@ function userCan($permission, \BookStack\Ownable $ownable = null) $permissionBaseName = strtolower($permission) . '-'; $hasPermission = false; if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true; - if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true; + if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true; - if(!$ownable instanceof \BookStack\Entity) return $hasPermission; + if (!$ownable instanceof \BookStack\Entity) return $hasPermission; // Check restrictions on the entitiy $restrictionService = app('BookStack\Services\RestrictionService'); diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index aa70eaa0a..328971f26 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder public function run() { $user = factory(BookStack\User::class, 1)->create(); - $role = \BookStack\Role::getDefault(); + $role = \BookStack\Role::getRole('editor'); $user->attachRole($role); diff --git a/phpunit.xml b/phpunit.xml index 762fc2da7..66196e8cf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,6 +21,7 @@ + diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index 1edfc0037..87aa20046 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -87,6 +87,9 @@ header { padding-top: $-s; } } + .dropdown-container { + font-size: 0.9em; + } } form.search-box { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index f0bd3b1ea..09707ebc4 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -95,13 +95,14 @@ // Sidebar list .book-tree { - padding: $-xl 0 0 0; + padding: $-l 0 0 0; position: relative; right: 0; top: 0; transition: ease-in-out 240ms; transition-property: right, border; border-left: 0px solid #FFF; + background-color: #FFF; &.fixed { position: fixed; top: 0; diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index d5d7cb139..7b5c92b5a 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -30,7 +30,9 @@ {!! $books->render() !!} @else

No books have been created.

- Create one now + @if(userCan('books-create-all')) + Create one now + @endif @endif
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 16/20] 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 17/20] 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); + } + } From e744d4c82ca3e4ae49b98b00bf0e048dde0f43e0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 6 Mar 2016 10:52:10 +0000 Subject: [PATCH 18/20] Changed color picker library and moved color logic to front end Since the old library was GPLv3 i changed the color picker to tiny-color-picker which is MIT. Also extracted the styles to a shared view and move color calculation logic to javascript side. --- app/Http/Controllers/SettingController.php | 64 ++----------------- .../jq-color-picker/tiny-color-picker.min.js | 4 ++ public/libs/jscolor/jscolor.min.js | 10 --- readme.md | 1 + resources/assets/js/global.js | 2 +- resources/assets/sass/styles.scss | 4 +- resources/views/base.blade.php | 23 +------ .../views/partials/custom-styles.blade.php | 22 +++++++ resources/views/public.blade.php | 14 +--- resources/views/settings/index.blade.php | 25 +++++++- 10 files changed, 60 insertions(+), 109 deletions(-) create mode 100644 public/libs/jq-color-picker/tiny-color-picker.min.js delete mode 100644 public/libs/jscolor/jscolor.min.js create mode 100644 resources/views/partials/custom-styles.blade.php diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index dfda2b6cf..1791ccfac 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,18 +1,14 @@ -checkPermission('settings-manage'); // Cycles through posted settings and update them - foreach($request->all() as $name => $value) { - if(strpos($name, 'setting-') !== 0) continue; + foreach ($request->all() as $name => $value) { + if (strpos($name, 'setting-') !== 0) continue; $key = str_replace('setting-', '', trim($name)); - if($key == 'app-color') { - Setting::put('app-color-rgba', $this->hex2rgba($value, 0.15)); - } Setting::put($key, $value); } @@ -48,51 +39,4 @@ class SettingController extends Controller return redirect('/settings'); } - /** - * Adapted from http://mekshq.com/how-to-convert-hexadecimal-color-code-to-rgb-or-rgba-using-php/ - * Converts a hex color code in to an RGBA string. - * - * @param string $color - * @param float|boolean $opacity - * @return boolean|string - */ - protected function hex2rgba($color, $opacity = false) - { - // Return false if no color provided - if(empty($color)) { - return false; - } - // Trim any whitespace - $color = trim($color); - - // Sanitize $color if "#" is provided - if($color[0] == '#' ) { - $color = substr($color, 1); - } - - // Check if color has 6 or 3 characters and get values - if(strlen($color) == 6) { - $hex = array( $color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5] ); - } elseif( strlen( $color ) == 3 ) { - $hex = array( $color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2] ); - } else { - return false; - } - - // Convert hexadec to rgb - $rgb = array_map('hexdec', $hex); - - // Check if opacity is set(rgba or rgb) - if($opacity) { - if(abs($opacity) > 1) - $opacity = 1.0; - $output = 'rgba('.implode(",",$rgb).','.$opacity.')'; - } else { - $output = 'rgb('.implode(",",$rgb).')'; - } - - // Return rgb(a) color string - return $output; - } - } diff --git a/public/libs/jq-color-picker/tiny-color-picker.min.js b/public/libs/jq-color-picker/tiny-color-picker.min.js new file mode 100644 index 000000000..84024073f --- /dev/null +++ b/public/libs/jq-color-picker/tiny-color-picker.min.js @@ -0,0 +1,4 @@ +/*! tinyColorPicker - v1.0.0 2016-02-28 */ +// https://github.com/PitPik/tinyColorPicker +// http://www.dematte.at/tinyColorPicker/index.html?type=small#demo +!function(a,b){"object"==typeof exports?module.exports=b(a):"function"==typeof define&&define.amd?define([],function(){return b(a)}):a.Colors=b(a)}(this,function(a,b){"use strict";function c(a,c,d,f,g){if("string"==typeof c){var c=t.txt2color(c);d=c.type,n[d]=c[d],g=g!==b?g:c.alpha}else if(c)for(var h in c)a[d][h]=k(c[h]/l[d][h][1],0,1);return g!==b&&(a.alpha=k(+g,0,1)),e(d,f?a:b)}function d(a,b,c){var d=m.options.grey,e={};return e.RGB={r:a.r,g:a.g,b:a.b},e.rgb={r:b.r,g:b.g,b:b.b},e.alpha=c,e.equivalentGrey=Math.round(d.r*a.r+d.g*a.g+d.b*a.b),e.rgbaMixBlack=i(b,{r:0,g:0,b:0},c,1),e.rgbaMixWhite=i(b,{r:1,g:1,b:1},c,1),e.rgbaMixBlack.luminance=h(e.rgbaMixBlack,!0),e.rgbaMixWhite.luminance=h(e.rgbaMixWhite,!0),m.options.customBG&&(e.rgbaMixCustom=i(b,m.options.customBG,c,1),e.rgbaMixCustom.luminance=h(e.rgbaMixCustom,!0),m.options.customBG.luminance=h(m.options.customBG,!0)),e}function e(a,b){var c,e,k,o=b||n,p=t,q=m.options,r=l,s=o.RND,u="",v="",w={hsl:"hsv",rgb:a},x=s.rgb;if("alpha"!==a){for(var y in r)if(!r[y][y]){a!==y&&(v=w[y]||"rgb",o[y]=p[v+"2"+y](o[v])),s[y]||(s[y]={}),c=o[y];for(u in c)s[y][u]=Math.round(c[u]*r[y][u][1])}x=s.rgb,o.HEX=p.RGB2HEX(x),o.equivalentGrey=q.grey.r*o.rgb.r+q.grey.g*o.rgb.g+q.grey.b*o.rgb.b,o.webSave=e=f(x,51),o.webSmart=k=f(x,17),o.saveColor=x.r===e.r&&x.g===e.g&&x.b===e.b?"web save":x.r===k.r&&x.g===k.g&&x.b===k.b?"web smart":"",o.hueRGB=t.hue2RGB(o.hsv.h),b&&(o.background=d(x,o.rgb,o.alpha))}var z,A,B,C=o.rgb,D=o.alpha,E="luminance",F=o.background;return z=i(C,{r:0,g:0,b:0},D,1),z[E]=h(z,!0),o.rgbaMixBlack=z,A=i(C,{r:1,g:1,b:1},D,1),A[E]=h(A,!0),o.rgbaMixWhite=A,q.customBG&&(B=i(C,F.rgbaMixCustom,D,1),B[E]=h(B,!0),B.WCAG2Ratio=j(B[E],F.rgbaMixCustom[E]),o.rgbaMixBGMixCustom=B,B.luminanceDelta=Math.abs(B[E]-F.rgbaMixCustom[E]),B.hueDelta=g(F.rgbaMixCustom,B,!0)),o.RGBLuminance=h(x),o.HUELuminance=h(o.hueRGB),q.convertCallback&&q.convertCallback(o,a),o}function f(a,b){var c={},d=0,e=b/2;for(var f in a)d=a[f]%b,c[f]=a[f]+(d>e?b-d:-d);return c}function g(a,b,c){return(Math.max(a.r-b.r,b.r-a.r)+Math.max(a.g-b.g,b.g-a.g)+Math.max(a.b-b.b,b.b-a.b))*(c?255:1)/765}function h(a,b){for(var c=b?1:255,d=[a.r/c,a.g/c,a.b/c],e=m.options.luminance,f=d.length;f--;)d[f]=d[f]<=.03928?d[f]/12.92:Math.pow((d[f]+.055)/1.055,2.4);return e.r*d[0]+e.g*d[1]+e.b*d[2]}function i(a,c,d,e){var f={},g=d!==b?d:1,h=e!==b?e:1,i=g+h*(1-g);for(var j in a)f[j]=(a[j]*g+c[j]*h*(1-g))/i;return f.a=i,f}function j(a,b){var c=1;return c=a>=b?(a+.05)/(b+.05):(b+.05)/(a+.05),Math.round(100*c)/100}function k(a,b,c){return a>c?c:b>a?b:a}var l={rgb:{r:[0,255],g:[0,255],b:[0,255]},hsv:{h:[0,360],s:[0,100],v:[0,100]},hsl:{h:[0,360],s:[0,100],l:[0,100]},alpha:{alpha:[0,1]},HEX:{HEX:[0,16777215]}},m={},n={},o={r:.298954,g:.586434,b:.114612},p={r:.2126,g:.7152,b:.0722},q=function(a){this.colors={RND:{}},this.options={color:"rgba(204, 82, 37, 0.8)",grey:o,luminance:p,valueRanges:l},r(this,a||{})},r=function(a,d){var e,f=a.options;s(a);for(var g in d)d[g]!==b&&(f[g]=d[g]);e=f.customBG,f.customBG="string"==typeof e?t.txt2color(e).rgb:e,n=c(a.colors,f.color,b,!0)},s=function(a){m!==a&&(m=a,n=a.colors)};q.prototype.setColor=function(a,d,f){return s(this),a?c(this.colors,a,d,b,f):(f!==b&&(this.colors.alpha=k(f,0,1)),e(d))},q.prototype.setCustomBackground=function(a){return s(this),this.options.customBG="string"==typeof a?t.txt2color(a).rgb:a,c(this.colors,b,"rgb")},q.prototype.saveAsBackground=function(){return s(this),c(this.colors,b,"rgb",!0)};var t={txt2color:function(a){var b={},c=a.replace(/(?:#|\)|%)/g,"").split("("),d=(c[1]||"").split(/,\s*/),e=c[1]?c[0].substr(0,3):"rgb",f="";if(b.type=e,b[e]={},c[1])for(var g=3;g--;)f=e[g]||e.charAt(g),b[e][f]=+d[g]/l[e][f][1];else b.rgb=t.HEX2rgb(c[0]);return b.alpha=d[3]?+d[3]:1,b},RGB2HEX:function(a){return((a.r<16?"0":"")+a.r.toString(16)+(a.g<16?"0":"")+a.g.toString(16)+(a.b<16?"0":"")+a.b.toString(16)).toUpperCase()},HEX2rgb:function(a){return a=a.split(""),{r:parseInt(a[0]+a[a[3]?1:0],16)/255,g:parseInt(a[a[3]?2:1]+(a[3]||a[1]),16)/255,b:parseInt((a[4]||a[2])+(a[5]||a[2]),16)/255}},hue2RGB:function(a){var b=6*a,c=~~b%6,d=6===b?0:b-c;return{r:Math.round(255*[1,1-d,0,0,d,1][c]),g:Math.round(255*[d,1,1,1-d,0,0][c]),b:Math.round(255*[0,0,d,1,1,1-d][c])}},rgb2hsv:function(a){var b,c,d,e=a.r,f=a.g,g=a.b,h=0;return g>f&&(f=g+(g=f,0),h=-1),c=g,f>e&&(e=f+(f=e,0),h=-2/6-h,c=Math.min(f,g)),b=e-c,d=e?b/e:0,{h:1e-15>d?n&&n.hsl&&n.hsl.h||0:b?Math.abs(h+(f-g)/(6*b)):0,s:e?b/e:n&&n.hsv&&n.hsv.s||0,v:e}},hsv2rgb:function(a){var b=6*a.h,c=a.s,d=a.v,e=~~b,f=b-e,g=d*(1-c),h=d*(1-f*c),i=d*(1-(1-f)*c),j=e%6;return{r:[d,h,g,g,i,d][j],g:[i,d,d,h,g,g][j],b:[g,g,i,d,d,h][j]}},hsv2hsl:function(a){var b=(2-a.s)*a.v,c=a.s*a.v;return c=a.s?1>b?b?c/b:0:c/(2-b):0,{h:a.h,s:a.v||c?c:n&&n.hsl&&n.hsl.s||0,l:b/2}},rgb2hsl:function(a,b){var c=t.rgb2hsv(a);return t.hsv2hsl(b?c:n.hsv=c)},hsl2rgb:function(a){var b=6*a.h,c=a.s,d=a.l,e=.5>d?d*(1+c):d+c-c*d,f=d+d-e,g=e?(e-f)/e:0,h=~~b,i=b-h,j=e*g*i,k=f+j,l=e-j,m=h%6;return{r:[e,l,f,f,k,e][m],g:[k,e,e,l,f,f][m],b:[f,f,k,e,e,l][m]}}};return q}),function(a,b){"object"==typeof exports?module.exports=b(a,require("jquery"),require("colors")):"function"==typeof define&&define.amd?define(["jquery","colors"],function(c,d){return b(a,c,d)}):b(a,a.jQuery,a.Colors)}(this,function(a,b,c,d){"use strict";function e(a){return a.value||a.getAttribute("value")||b(a).css("background-color")||"#fff"}function f(a){return a=a.originalEvent&&a.originalEvent.touches?a.originalEvent.touches[0]:a,a.originalEvent?a.originalEvent:a}function g(a){return b(a.find(r.doRender)[0]||a[0])}function h(c){var d=b(this),f=d.offset(),h=b(a),j=r.gap;c?(s=g(d),s._colorMode=s.data("colorMode"),p.$trigger=d,(t||i()).css({left:(t[0]._left=f.left)-((t[0]._left=t[0]._left+t[0]._width-(h.scrollLeft()+h.width()))+j>0?t[0]._left+j:0),top:(t[0]._top=f.top+d.outerHeight())-((t[0]._top=t[0]._top+t[0]._height-(h.scrollTop()+h.height()))+j>0?t[0]._top+j:0)}).show(r.animationSpeed,function(){c!==!0&&(x._width=x.width(),u._width=u.width(),u._height=u.height(),q.setColor(e(s[0])),n(!0))})):b(t).hide(r.animationSpeed,function(){n(!1),p.$trigger=null})}function i(){return b("head").append('"),p.$UI=t=b(H).css({margin:r.margin}).appendTo("body").show(0,function(){var a=b(this);E=r.GPU&&a.css("perspective")!==d,u=b(".cp-xy-slider",this),v=b(".cp-xy-cursor",this),w=b(".cp-z-cursor",this),x=b(".cp-alpha",this).toggle(!!r.opacity),y=b(".cp-alpha-cursor",this),r.buildCallback.call(p,a),a.prepend("
").children().eq(0).css("width",a.children().eq(0).width()),this._width=this.offsetWidth,this._height=this.offsetHeight}).hide().on(C,".cp-xy-slider,.cp-z-slider,.cp-alpha",j)}function j(a){var c=this.className.replace(/cp-(.*?)(?:\s*|$)/,"$1").replace("-","_");(a.button||a.which)>1||(a.preventDefault&&a.preventDefault(),a.returnValue=!1,s._offset=b(this).offset(),(c="xy_slider"===c?k:"z_slider"===c?l:m)(a),n(),z.on(D,function(){z.off(".a")}).on(B,function(a){c(a),n()}))}function k(a){var b=f(a),c=b.pageX-s._offset.left,d=b.pageY-s._offset.top;q.setColor({s:c/u._width*100,v:100-d/u._height*100},"hsv")}function l(a){var b=f(a).pageY-s._offset.top;q.setColor({h:360-b/u._height*360},"hsv")}function m(a){var b=f(a).pageX-s._offset.left,c=b/x._width;q.setColor({},"rgb",c)}function n(a){var b=q.colors,c=b.hueRGB,e=b.RND.rgb,f=b.RND.hsl,g="#222",h="#ddd",i=s._colorMode,j=1!==b.alpha,k=F(100*b.alpha)/100,l=e.r+", "+e.g+", "+e.b,m="HEX"!==i||j?"rgb"===i||"HEX"===i&&j?j?"rgba("+l+", "+k+")":"rgb("+l+")":"hsl"+(j?"a(":"(")+f.h+", "+f.s+"%, "+f.l+"%"+(j?", "+k:"")+")":"#"+b.HEX,n=b.HUELuminance>.22?g:h,p=b.rgbaMixBlack.luminance>.22?g:h,r=(1-b.hsv.h)*u._height,t=b.hsv.s*u._width,z=(1-b.hsv.v)*u._height,A=k*x._width,B=E?"translate3d":"",C=s[0].value,D=s[0].hasAttribute("value")&&""===C&&a!==d;u._css={backgroundColor:"rgb("+c.r+","+c.g+","+c.b+")"},v._css={transform:B+"("+t+"px, "+z+"px, 0)",left:E?"":t,top:E?"":z,borderColor:b.RGBLuminance>.22?g:h},w._css={transform:B+"(0, "+r+"px, 0)",top:E?"":r,borderColor:"transparent "+n},x._css={backgroundColor:"rgb("+l+")"},y._css={transform:B+"("+A+"px, 0, 0)",left:E?"":A,borderColor:p+" transparent"},s._css={backgroundColor:D?"":m,color:D?"":b.rgbaMixBGMixCustom.luminance>.22?g:h},s.text=D?"":C!==m?m:"",a!==d?o(a):G(o)}function o(a){u.css(u._css),v.css(v._css),w.css(w._css),x.css(x._css),y.css(y._css),r.doRender&&s.css(s._css),s.text&&s.val(s.text),r.renderCallback.call(p,s,"boolean"==typeof a?a:d)}var p,q,r,s,t,u,v,w,x,y,z=b(document),A=b(),B="touchmove.a mousemove.a pointermove.a",C="touchstart.a mousedown.a pointerdown.a",D="touchend.a mouseup.a pointerup.a",E=!1,F=Math.round,G=a.requestAnimationFrame||a.webkitRequestAnimationFrame||function(a){a()},H='
',I=".cp-color-picker{position:absolute;overflow:hidden;padding:6px 6px 0;background-color:#444;color:#bbb;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:400;cursor:default;border-radius:5px}.cp-color-picker>div{position:relative;overflow:hidden}.cp-xy-slider{float:left;height:128px;width:128px;margin-bottom:6px;background:linear-gradient(to right,#FFF,rgba(255,255,255,0))}.cp-white{height:100%;width:100%;background:linear-gradient(rgba(0,0,0,0),#000)}.cp-xy-cursor{position:absolute;top:0;width:10px;height:10px;margin:-5px;border:1px solid #fff;border-radius:100%;box-sizing:border-box}.cp-z-slider{float:right;margin-left:6px;height:128px;width:20px;background:linear-gradient(red 0,#f0f 17%,#00f 33%,#0ff 50%,#0f0 67%,#ff0 83%,red 100%)}.cp-z-cursor{position:absolute;margin-top:-4px;width:100%;border:4px solid #fff;border-color:transparent #fff;box-sizing:border-box}.cp-alpha{clear:both;width:100%;height:16px;margin:6px 0;background:linear-gradient(to right,#444,rgba(0,0,0,0))}.cp-alpha-cursor{position:absolute;margin-left:-4px;height:100%;border:4px solid #fff;border-color:#fff transparent;box-sizing:border-box}",J=function(a){q=this.color=new c(a),r=q.options,p=this};return J.prototype={render:n,toggle:h},b.fn.colorPicker=function(c){var d=function(){};return c=b.extend({animationSpeed:150,GPU:!0,doRender:!0,customBG:"#FFF",opacity:!0,renderCallback:d,buildCallback:d,body:document.body,scrollResize:!0,gap:4},c),!p&&c.scrollResize&&b(a).on("resize.a scroll.a",function(){p.$trigger&&p.toggle.call(p.$trigger[0],!0)}),A=A.add(this),this.colorPicker=A.colorPicker=p||new J(c),b(c.body).off(".a").on(C,function(a){!A.add(t).find(a.target).add(A.filter(a.target))[0]&&h()}),this.on("focusin.a click.a",h).on("change.a",function(){q.setColor(this.value||"#FFF"),A.colorPicker.render(!0)}).each(function(){var a=e(this),d=a.split("("),f=g(b(this));f.data("colorMode",d[1]?d[0].substr(0,3):"HEX").attr("readonly",r.preventFocus),c.doRender&&f.css({"background-color":a,color:function(){return q.setColor(a).rgbaMixBGMixCustom.luminance>.22?"#222":"#ddd"}})})},b.fn.colorPicker.destroy=function(){A.add(r.body).off(".a"),p.toggle(!1),A=b()},b}); \ No newline at end of file diff --git a/public/libs/jscolor/jscolor.min.js b/public/libs/jscolor/jscolor.min.js deleted file mode 100644 index 2a7a788be..000000000 --- a/public/libs/jscolor/jscolor.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * jscolor - JavaScript Color Picker - * - * @link http://jscolor.com - * @license For open source use: GPLv3 - * For commercial use: JSColor Commercial License - * @author Jan Odvarko - * - * See usage examples at http://jscolor.com/examples/ - */"use strict";window.jscolor||(window.jscolor=function(){var e={register:function(){e.attachDOMReadyEvent(e.init),e.attachEvent(document,"mousedown",e.onDocumentMouseDown),e.attachEvent(document,"touchstart",e.onDocumentTouchStart),e.attachEvent(window,"resize",e.onWindowResize)},init:function(){e.jscolor.lookupClass&&e.jscolor.installByClassName(e.jscolor.lookupClass)},tryInstallOnElements:function(t,n){var r=new RegExp("(^|\\s)("+n+")(\\s*(\\{[^}]*\\})|\\s|$)","i");for(var i=0;is[u]?-r[u]+n[u]+i[u]/2>s[u]/2&&n[u]+i[u]-o[u]>=0?n[u]+i[u]-o[u]:n[u]:n[u],-r[a]+n[a]+i[a]+o[a]-l+l*f>s[a]?-r[a]+n[a]+i[a]/2>s[a]/2&&n[a]+i[a]-l-l*f>=0?n[a]+i[a]-l-l*f:n[a]+i[a]-l+l*f:n[a]+i[a]-l+l*f>=0?n[a]+i[a]-l+l*f:n[a]+i[a]-l-l*f];var h=c[u],p=c[a],d=t.fixed?"fixed":"absolute",v=(c[0]+o[0]>n[0]||c[0]2)switch(e.mode.charAt(2).toLowerCase()){case"s":return"s";case"v":return"v"}return null},onDocumentMouseDown:function(t){t||(t=window.event);var n=t.target||t.srcElement;n._jscLinkedInstance?n._jscLinkedInstance.showOnClick&&n._jscLinkedInstance.show():n._jscControlName?e.onControlPointerStart(t,n,n._jscControlName,"mouse"):e.picker&&e.picker.owner&&e.picker.owner.hide()},onDocumentTouchStart:function(t){t||(t=window.event);var n=t.target||t.srcElement;n._jscLinkedInstance?n._jscLinkedInstance.showOnClick&&n._jscLinkedInstance.show():n._jscControlName?e.onControlPointerStart(t,n,n._jscControlName,"touch"):e.picker&&e.picker.owner&&e.picker.owner.hide()},onWindowResize:function(t){e.redrawPosition()},onParentScroll:function(t){e.picker&&e.picker.owner&&e.picker.owner.hide()},_pointerMoveEvent:{mouse:"mousemove",touch:"touchmove"},_pointerEndEvent:{mouse:"mouseup",touch:"touchend"},_pointerOrigin:null,_capturedTarget:null,onControlPointerStart:function(t,n,r,i){var s=n._jscInstance;e.preventDefault(t),e.captureTarget(n);var o=function(s,o){e.attachGroupEvent("drag",s,e._pointerMoveEvent[i],e.onDocumentPointerMove(t,n,r,i,o)),e.attachGroupEvent("drag",s,e._pointerEndEvent[i],e.onDocumentPointerEnd(t,n,r,i))};o(document,[0,0]);if(window.parent&&window.frameElement){var u=window.frameElement.getBoundingClientRect(),a=[-u.left,-u.top];o(window.parent.window.document,a)}var f=e.getAbsPointerPos(t),l=e.getRelPointerPos(t);e._pointerOrigin={x:f.x-l.x,y:f.y-l.y};switch(r){case"pad":switch(e.getSliderComponent(s)){case"s":s.hsv[1]===0&&s.fromHSV(null,100,null);break;case"v":s.hsv[2]===0&&s.fromHSV(null,null,100)}e.setPad(s,t,0,0);break;case"sld":e.setSld(s,t,0)}e.dispatchFineChange(s)},onDocumentPointerMove:function(t,n,r,i,s){return function(t){var i=n._jscInstance;switch(r){case"pad":t||(t=window.event),e.setPad(i,t,s[0],s[1]),e.dispatchFineChange(i);break;case"sld":t||(t=window.event),e.setSld(i,t,s[1]),e.dispatchFineChange(i)}}},onDocumentPointerEnd:function(t,n,r,i){return function(t){var r=n._jscInstance;e.detachGroupEvents("drag"),e.releaseTarget(),e.dispatchChange(r)}},dispatchChange:function(t){t.valueElement&&e.isElementType(t.valueElement,"input")&&e.fireEvent(t.valueElement,"change")},dispatchFineChange:function(e){if(e.onFineChange){var t;typeof e.onFineChange=="string"?t=new Function(e.onFineChange):t=e.onFineChange,t.call(e)}},setPad:function(t,n,r,i){var s=e.getAbsPointerPos(n),o=r+s.x-e._pointerOrigin.x-t.padding-t.insetWidth,u=i+s.y-e._pointerOrigin.y-t.padding-t.insetWidth,a=o*(360/(t.width-1)),f=100-u*(100/(t.height-1));switch(e.getPadYComponent(t)){case"s":t.fromHSV(a,f,null,e.leaveSld);break;case"v":t.fromHSV(a,null,f,e.leaveSld)}},setSld:function(t,n,r){var i=e.getAbsPointerPos(n),s=r+i.y-e._pointerOrigin.y-t.padding-t.insetWidth,o=100-s*(100/(t.height-1));switch(e.getSliderComponent(t)){case"s":t.fromHSV(null,o,null,e.leavePad);break;case"v":t.fromHSV(null,null,o,e.leavePad)}},_vmlNS:"jsc_vml_",_vmlCSS:"jsc_vml_css_",_vmlReady:!1,initVML:function(){if(!e._vmlReady){var t=document;t.namespaces[e._vmlNS]||t.namespaces.add(e._vmlNS,"urn:schemas-microsoft-com:vml");if(!t.styleSheets[e._vmlCSS]){var n=["shape","shapetype","group","background","path","formulas","handles","fill","stroke","shadow","textbox","textpath","imagedata","line","polyline","curve","rect","roundrect","oval","arc","image"],r=t.createStyleSheet();r.owningElement.id=e._vmlCSS;for(var i=0;i=3&&(s=r[0].match(i))&&(o=r[1].match(i))&&(u=r[2].match(i))){var a=parseFloat((s[1]||"0")+(s[2]||"")),f=parseFloat((o[1]||"0")+(o[2]||"")),l=parseFloat((u[1]||"0")+(u[2]||""));return this.fromRGB(a,f,l,t),!0}}return!1},this.toString=function(){return(256|Math.round(this.rgb[0])).toString(16).substr(1)+(256|Math.round(this.rgb[1])).toString(16).substr(1)+(256|Math.round(this.rgb[2])).toString(16).substr(1)},this.toHEXString=function(){return"#"+this.toString().toUpperCase()},this.toRGBString=function(){return"rgb("+Math.round(this.rgb[0])+","+Math.round(this.rgb[1])+","+Math.round(this.rgb[2])+")"},this.isLight=function(){return.213*this.rgb[0]+.715*this.rgb[1]+.072*this.rgb[2]>127.5},this._processParentElementsInDOM=function(){if(this._linkedElementsProcessed)return;this._linkedElementsProcessed=!0;var t=this.targetElement;do{var n=e.getStyle(t);n&&n.position.toLowerCase()==="fixed"&&(this.fixed=!0),t!==this.targetElement&&(t._jscEventsAttached||(e.attachEvent(t,"scroll",e.onParentScroll),t._jscEventsAttached=!0))}while((t=t.parentNode)&&!e.isElementType(t,"body"))};if(typeof t=="string"){var h=t,p=document.getElementById(h);p?this.targetElement=p:e.warn("Could not find target element with ID '"+h+"'")}else t?this.targetElement=t:e.warn("Invalid target element: '"+t+"'");if(this.targetElement._jscLinkedInstance){e.warn("Cannot link jscolor twice to the same element. Skipping.");return}this.targetElement._jscLinkedInstance=this,this.valueElement=e.fetchElement(this.valueElement),this.styleElement=e.fetchElement(this.styleElement);var d=this,v=this.container?e.fetchElement(this.container):document.getElementsByTagName("body")[0],m=3;if(e.isElementType(this.targetElement,"button"))if(this.targetElement.onclick){var g=this.targetElement.onclick;this.targetElement.onclick=function(e){return g.call(this,e),!1}}else this.targetElement.onclick=function(){return!1};if(this.valueElement&&e.isElementType(this.valueElement,"input")){var y=function(){d.fromString(d.valueElement.value,e.leaveValue),e.dispatchFineChange(d)};e.attachEvent(this.valueElement,"keyup",y),e.attachEvent(this.valueElement,"input",y),e.attachEvent(this.valueElement,"blur",c),this.valueElement.setAttribute("autocomplete","off")}this.styleElement&&(this.styleElement._jscOrigStyle={backgroundImage:this.styleElement.style.backgroundImage,backgroundColor:this.styleElement.style.backgroundColor,color:this.styleElement.style.color}),this.value?this.fromString(this.value)||this.exportColor():this.importColor()}};return e.jscolor.lookupClass="jscolor",e.jscolor.installByClassName=function(t){var n=document.getElementsByTagName("input"),r=document.getElementsByTagName("button");e.tryInstallOnElements(n,t),e.tryInstallOnElements(r,t)},e.register(),e.jscolor}()); \ No newline at end of file diff --git a/readme.md b/readme.md index a191e1694..0730e3de3 100644 --- a/readme.md +++ b/readme.md @@ -175,3 +175,4 @@ These are the great projects used to help build BookStack: * [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html) * [Dropzone.js](http://www.dropzonejs.com/) * [ZeroClipboard](http://zeroclipboard.org/) +* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 90b03e856..5400a8af0 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -95,7 +95,7 @@ $(function () { scrollTop.style.display = 'block'; scrollTopShowing = true; setTimeout(() => { - scrollTop.style.opacity = 1; + scrollTop.style.opacity = 0.4; }, 1); } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) { scrollTop.style.opacity = 0; diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 4e6823fc0..9c4a4dafc 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -138,7 +138,7 @@ $loadingSize: 10px; // Back to top link $btt-size: 40px; #back-to-top { - background-color: rgba($primary, 0.4); + background-color: $primary; position: fixed; bottom: $-m; right: $-l; @@ -154,7 +154,7 @@ $btt-size: 40px; overflow: hidden; &:hover { width: $btt-size*3.4; - background-color: rgba($primary, 1); + opacity: 1 !important; span { display: inline-block; } diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 59bbe6fbb..109d373f9 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -17,25 +17,8 @@ @yield('head') - @if(Setting::get('app-color')) - - @endif + + @include('partials/custom-styles') @@ -62,7 +45,7 @@
-

This should be a hex value.

- +

This should be a hex value.
Leave empty to reset to the default color.

+ +
@@ -96,5 +97,23 @@ @stop @section('scripts') - + + @stop \ No newline at end of file From 66c56e9d02efe5ee6c77fcb41ba82176c65f5475 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 6 Mar 2016 12:55:08 +0000 Subject: [PATCH 19/20] Added settings helper and formatted code in some files --- app/Entity.php | 4 ++-- app/Http/Controllers/Auth/AuthController.php | 24 ++++++++++---------- app/Http/Middleware/Authenticate.php | 2 +- app/Repos/PermissionsRepo.php | 2 +- app/Repos/UserRepo.php | 2 +- app/Services/EmailConfirmationService.php | 2 +- app/Services/ImageService.php | 2 +- app/Services/SocialAuthService.php | 2 +- app/helpers.php | 14 +++++++++++- resources/views/base.blade.php | 8 +++---- 10 files changed, 37 insertions(+), 25 deletions(-) diff --git a/app/Entity.php b/app/Entity.php index 6bf29ca0f..4f97c6bab 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -107,7 +107,7 @@ abstract class Entity extends Ownable $exactTerms = []; foreach ($terms as $key => $term) { $term = htmlentities($term, ENT_QUOTES); - $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); if (preg_match('/\s/', $term)) { $exactTerms[] = '%' . $term . '%'; $term = '"' . $term . '"'; @@ -123,7 +123,7 @@ abstract class Entity extends Ownable // Ensure at least one exact term matches if in search if (count($exactTerms) > 0) { - $search = $search->where(function($query) use ($exactTerms, $fieldsToSearch) { + $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { foreach ($exactTerms as $exactTerm) { foreach ($fieldsToSearch as $field) { $query->orWhere($field, 'like', $exactTerm); diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index fef87d5c8..fda0ee668 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -41,9 +41,9 @@ class AuthController extends Controller /** * Create a new authentication controller instance. - * @param SocialAuthService $socialAuthService + * @param SocialAuthService $socialAuthService * @param EmailConfirmationService $emailConfirmationService - * @param UserRepo $userRepo + * @param UserRepo $userRepo */ public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) { @@ -63,15 +63,15 @@ class AuthController extends Controller protected function validator(array $data) { return Validator::make($data, [ - 'name' => 'required|max:255', - 'email' => 'required|email|max:255|unique:users', + 'name' => 'required|max:255', + 'email' => 'required|email|max:255|unique:users', 'password' => 'required|min:6', ]); } protected function checkRegistrationAllowed() { - if (!\Setting::get('registration-enabled')) { + if (!setting('registration-enabled')) { throw new UserRegistrationException('Registrations are currently disabled.', '/login'); } } @@ -112,7 +112,7 @@ class AuthController extends Controller /** * Overrides the action when a user is authenticated. * If the user authenticated but does not exist in the user table we create them. - * @param Request $request + * @param Request $request * @param Authenticatable $user * @return \Illuminate\Http\RedirectResponse */ @@ -153,8 +153,8 @@ class AuthController extends Controller // Create an array of the user data to create a new user instance $userData = [ - 'name' => $socialUser->getName(), - 'email' => $socialUser->getEmail(), + 'name' => $socialUser->getName(), + 'email' => $socialUser->getEmail(), 'password' => str_random(30) ]; return $this->registerUser($userData, $socialAccount); @@ -162,7 +162,7 @@ class AuthController extends Controller /** * The registrations flow for all users. - * @param array $userData + * @param array $userData * @param bool|false|SocialAccount $socialAccount * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException @@ -170,8 +170,8 @@ class AuthController extends Controller */ protected function registerUser(array $userData, $socialAccount = false) { - if (\Setting::get('registration-restrict')) { - $restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict'))); + if (setting('registration-restrict')) { + $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict'))); $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1); if (!in_array($userEmailDomain, $restrictedEmailDomains)) { throw new UserRegistrationException('That email domain does not have access to this application', '/register'); @@ -183,7 +183,7 @@ class AuthController extends Controller $newUser->socialAccounts()->save($socialAccount); } - if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) { + if (setting('registration-confirmation') || setting('registration-restrict')) { $newUser->email_confirmed = false; $newUser->save(); $this->emailConfirmationService->sendConfirmation($newUser); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index ad804d0d8..81392fe6e 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -39,7 +39,7 @@ class Authenticate return redirect()->guest('/register/confirm/awaiting'); } - if ($this->auth->guest() && !Setting::get('app-public')) { + if ($this->auth->guest() && !setting('app-public')) { if ($request->ajax()) { return response('Unauthorized.', 401); } else { diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index 2d497b76a..3c5efde23 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -124,7 +124,7 @@ class PermissionsRepo // Prevent deleting admin role or default registration role. if ($role->name === 'admin') { throw new PermissionsException('The admin role cannot be deleted'); - } else if ($role->id == Setting::get('registration-role')) { + } else if ($role->id == setting('registration-role')) { throw new PermissionsException('This role cannot be deleted while set as the default registration role.'); } diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index 01cf80d29..ec6f3d0d1 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -77,7 +77,7 @@ class UserRepo */ public function attachDefaultRole($user) { - $roleId = Setting::get('registration-role'); + $roleId = setting('registration-role'); if ($roleId === false) $roleId = $this->role->first()->id; $user->attachRoleId($roleId); } diff --git a/app/Services/EmailConfirmationService.php b/app/Services/EmailConfirmationService.php index ffe21eec4..c3096c654 100644 --- a/app/Services/EmailConfirmationService.php +++ b/app/Services/EmailConfirmationService.php @@ -45,7 +45,7 @@ class EmailConfirmationService 'token' => $token, ]); $this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) { - $appName = \Setting::get('app-name', 'BookStack'); + $appName = setting('app-name', 'BookStack'); $message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.'); }); } diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index 47c27cd0a..aefc8a4fb 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -79,7 +79,7 @@ class ImageService private function saveNew($imageName, $imageData, $type) { $storage = $this->getStorage(); - $secureUploads = Setting::get('app-secure-images'); + $secureUploads = setting('app-secure-images'); $imageName = str_replace(' ', '-', $imageName); if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php index 2437a4827..df213609a 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Services/SocialAuthService.php @@ -135,7 +135,7 @@ class SocialAuthService // Otherwise let the user know this social account is not used by anyone. $message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings'; - if (\Setting::get('registration-enabled')) { + if (setting('registration-enabled')) { $message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option'; } throw new SocialSignInException($message . '.', '/login'); diff --git a/app/helpers.php b/app/helpers.php index ead6b3008..f60e917c5 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -58,4 +58,16 @@ function userCan($permission, \BookStack\Ownable $ownable = null) $action = end($explodedPermission); $hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action); return $hasAccess && $hasPermission; -} \ No newline at end of file +} + +/** + * Helper to access system settings. + * @param $key + * @param bool $default + * @return mixed + */ +function setting($key, $default = false) +{ + $settingService = app('BookStack\Services\SettingService'); + return $settingService->get($key, $default); +} diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 109d373f9..5c744d821 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -1,7 +1,7 @@ - {{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ Setting::get('app-name', 'BookStack') }} + {{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name', 'BookStack') }} @@ -29,10 +29,10 @@
From 1d6137f7e2af3100bcb2b6bc58747c7be1838369 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 6 Mar 2016 13:17:46 +0000 Subject: [PATCH 20/20] Added restrictions to user profile lists --- app/Repos/EntityRepo.php | 51 +++++++++++++++++++++++++------- app/Repos/UserRepo.php | 19 +++++++----- app/Services/ActivityService.php | 19 ++++++------ 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 9c5184e2f..ea2805855 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -42,13 +42,19 @@ class EntityRepo /** * Get the latest books added to the system. - * @param $count - * @param $page + * @param int $count + * @param int $page + * @param bool $additionalQuery + * @return */ - public function getRecentlyCreatedBooks($count = 20, $page = 0) + public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false) { - return $this->restrictionService->enforceBookRestrictions($this->book) - ->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get(); + $query = $this->restrictionService->enforceBookRestrictions($this->book) + ->orderBy('created_at', 'desc'); + if ($additionalQuery !== false && is_callable($additionalQuery)) { + $additionalQuery($query); + } + return $query->skip($page * $count)->take($count)->get(); } /** @@ -65,13 +71,36 @@ class EntityRepo /** * Get the latest pages added to the system. - * @param $count - * @param $page + * @param int $count + * @param int $page + * @param bool $additionalQuery + * @return */ - public function getRecentlyCreatedPages($count = 20, $page = 0) + public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false) { - return $this->restrictionService->enforcePageRestrictions($this->page) - ->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get(); + $query = $this->restrictionService->enforcePageRestrictions($this->page) + ->orderBy('created_at', 'desc'); + if ($additionalQuery !== false && is_callable($additionalQuery)) { + $additionalQuery($query); + } + return $query->skip($page * $count)->take($count)->get(); + } + + /** + * Get the latest chapters added to the system. + * @param int $count + * @param int $page + * @param bool $additionalQuery + * @return + */ + public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false) + { + $query = $this->restrictionService->enforceChapterRestrictions($this->chapter) + ->orderBy('created_at', 'desc'); + if ($additionalQuery !== false && is_callable($additionalQuery)) { + $additionalQuery($query); + } + return $query->skip($page * $count)->take($count)->get(); } /** @@ -100,7 +129,7 @@ class EntityRepo foreach ($restrictions as $action => $value) { $entity->restrictions()->create([ 'role_id' => $roleId, - 'action' => strtolower($action) + 'action' => strtolower($action) ]); } } diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index ec6f3d0d1..d5a4b1503 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -141,12 +141,15 @@ class UserRepo public function getRecentlyCreated(User $user, $count = 20) { return [ - 'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->orderBy('created_at', 'desc') - ->take($count)->get(), - 'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->orderBy('created_at', 'desc') - ->take($count)->get(), - 'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->orderBy('created_at', 'desc') - ->take($count)->get() + 'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) { + $query->where('created_by', '=', $user->id); + }), + 'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) { + $query->where('created_by', '=', $user->id); + }), + 'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) { + $query->where('created_by', '=', $user->id); + }) ]; } @@ -158,9 +161,9 @@ class UserRepo public function getAssetCounts(User $user) { return [ - 'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(), + 'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(), 'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(), - 'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(), + 'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(), ]; } diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index 118bd6d9c..d0029b6c4 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -26,8 +26,8 @@ class ActivityService * Add activity data to database. * @param Entity $entity * @param $activityKey - * @param int $bookId - * @param bool $extra + * @param int $bookId + * @param bool $extra */ public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false) { @@ -45,7 +45,7 @@ class ActivityService /** * Adds a activity history with a message & without binding to a entity. * @param $activityKey - * @param int $bookId + * @param int $bookId * @param bool|false $extra */ public function addMessage($activityKey, $bookId = 0, $extra = false) @@ -88,7 +88,7 @@ class ActivityService */ public function latest($count = 20, $page = 0) { - $activityList = $this->restrictionService + $activityList = $this->restrictionService ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); @@ -99,8 +99,8 @@ class ActivityService * Gets the latest activity for an entity, Filtering out similar * items to prevent a message activity list. * @param Entity $entity - * @param int $count - * @param int $page + * @param int $count + * @param int $page * @return array */ public function entityActivity($entity, $count = 20, $page = 0) @@ -121,9 +121,10 @@ class ActivityService */ public function userActivity($user, $count = 20, $page = 0) { - $activity = $this->activity->where('user_id', '=', $user->id) - ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); - return $this->filterSimilar($activity); + $activityList = $this->restrictionService + ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') + ->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get(); + return $this->filterSimilar($activityList); } /**