Merge branch 'master' into nwalke-update_site_color

This commit is contained in:
Dan Brown 2016-03-06 09:08:20 +00:00
commit 0774ecc89c
74 changed files with 2865 additions and 346 deletions

View File

@ -12,8 +12,17 @@ DB_PASSWORD=database_user_password
# Cache and session # Cache and session
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_DRIVER=file SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync 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
STORAGE_TYPE=local STORAGE_TYPE=local
# Amazon S3 Config # Amazon S3 Config
@ -53,4 +62,4 @@ MAIL_HOST=localhost
MAIL_PORT=1025 MAIL_PORT=1025
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null

View File

@ -15,15 +15,11 @@ class Activity extends Model
/** /**
* Get the entity for this activity. * Get the entity for this activity.
* @return bool
*/ */
public function entity() public function entity()
{ {
if ($this->entity_id) { if ($this->entity_type === '') $this->entity_type = null;
return $this->morphTo('entity')->first(); return $this->morphTo('entity');
} else {
return false;
}
} }
/** /**

View File

@ -1,14 +1,9 @@
<?php <?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model; abstract class Entity extends Ownable
abstract class Entity extends Model
{ {
use Ownable;
/** /**
* Compares this entity to another given entity. * Compares this entity to another given entity.
* Matches by comparing class and id. * Matches by comparing class and id.
@ -53,13 +48,31 @@ abstract class Entity extends Model
/** /**
* Get View objects for this entity. * Get View objects for this entity.
* @return mixed
*/ */
public function views() public function views()
{ {
return $this->morphMany('BookStack\View', 'viewable'); return $this->morphMany('BookStack\View', 'viewable');
} }
/**
* Get this entities restrictions.
*/
public function restrictions()
{
return $this->morphMany('BookStack\Restriction', 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
* @param $role_id
* @param $action
* @return bool
*/
public function hasRestriction($role_id, $action)
{
return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
}
/** /**
* Allows checking of the exact class, Used to check entity type. * Allows checking of the exact class, Used to check entity type.
* Cleaner method for is_a. * Cleaner method for is_a.
@ -72,23 +85,14 @@ abstract class Entity extends Model
} }
/** /**
* Gets the class name. * Gets a limited-length version of the entities name.
* @return string
*/
public static function getClassName()
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
/**
*Gets a limited-length version of the entities name.
* @param int $length * @param int $length
* @return string * @return string
*/ */
public function getShortName($length = 25) public function getShortName($length = 25)
{ {
if(strlen($this->name) <= $length) return $this->name; if (strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length-3) . '...'; return substr($this->name, 0, $length - 3) . '...';
} }
/** /**
@ -100,22 +104,40 @@ abstract class Entity extends Model
*/ */
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
{ {
$termString = ''; $exactTerms = [];
foreach ($terms as $term) { foreach ($terms as $key => $term) {
$termString .= htmlentities($term) . '* '; $term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/\s/', $term)) {
$exactTerms[] = '%' . $term . '%';
$term = '"' . $term . '"';
} else {
$term = '' . $term . '*';
}
if ($term !== '*') $terms[$key] = $term;
} }
$termString = implode(' ', $terms);
$fields = implode(',', $fieldsToSearch); $fields = implode(',', $fieldsToSearch);
$termStringEscaped = \DB::connection()->getPdo()->quote($termString); $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$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 // Add additional where terms
foreach ($wheres as $whereTerm) { foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
} }
// Load in relations // Load in relations
if (static::isA('page')) { if (static::isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if (static::isA('chapter')) { } else if (static::isA('chapter')) {
$search = $search->with('book'); $search = $search->with('book');

View File

@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
// Which will include the basic message to point the user roughly to the cause. // 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')) { if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); $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); return parent::render($request, $e);

View File

@ -0,0 +1,14 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
/**
* NotFoundException constructor.
* @param string $message
*/
public function __construct($message = 'Item not found')
{
parent::__construct($message, 404);
}
}

View File

@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PermissionsException extends Exception {}

View File

@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -19,18 +20,21 @@ class BookController extends Controller
protected $bookRepo; protected $bookRepo;
protected $pageRepo; protected $pageRepo;
protected $chapterRepo; protected $chapterRepo;
protected $userRepo;
/** /**
* BookController constructor. * BookController constructor.
* @param BookRepo $bookRepo * @param BookRepo $bookRepo
* @param PageRepo $pageRepo * @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo * @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo
*/ */
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo) public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{ {
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
$this->pageRepo = $pageRepo; $this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo; $this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
} }
@ -55,7 +59,7 @@ class BookController extends Controller
*/ */
public function create() public function create()
{ {
$this->checkPermission('book-create'); $this->checkPermission('book-create-all');
$this->setPageTitle('Create New Book'); $this->setPageTitle('Create New Book');
return view('books/create'); return view('books/create');
} }
@ -68,9 +72,9 @@ class BookController extends Controller
*/ */
public function store(Request $request) public function store(Request $request)
{ {
$this->checkPermission('book-create'); $this->checkPermission('book-create-all');
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000' 'description' => 'string|max:1000'
]); ]);
$book = $this->bookRepo->newFromInput($request->all()); $book = $this->bookRepo->newFromInput($request->all());
@ -105,8 +109,8 @@ class BookController extends Controller
*/ */
public function edit($slug) public function edit($slug)
{ {
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug); $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle('Edit Book ' . $book->getShortName()); $this->setPageTitle('Edit Book ' . $book->getShortName());
return view('books/edit', ['book' => $book, 'current' => $book]); return view('books/edit', ['book' => $book, 'current' => $book]);
} }
@ -120,10 +124,10 @@ class BookController extends Controller
*/ */
public function update(Request $request, $slug) public function update(Request $request, $slug)
{ {
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug); $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000' 'description' => 'string|max:1000'
]); ]);
$book->fill($request->all()); $book->fill($request->all());
@ -141,8 +145,8 @@ class BookController extends Controller
*/ */
public function showDelete($bookSlug) public function showDelete($bookSlug)
{ {
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle('Delete Book ' . $book->getShortName()); $this->setPageTitle('Delete Book ' . $book->getShortName());
return view('books/delete', ['book' => $book, 'current' => $book]); return view('books/delete', ['book' => $book, 'current' => $book]);
} }
@ -154,8 +158,8 @@ class BookController extends Controller
*/ */
public function sort($bookSlug) public function sort($bookSlug)
{ {
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->bookRepo->getChildren($book); $bookChildren = $this->bookRepo->getChildren($book);
$books = $this->bookRepo->getAll(false); $books = $this->bookRepo->getAll(false);
$this->setPageTitle('Sort Book ' . $book->getShortName()); $this->setPageTitle('Sort Book ' . $book->getShortName());
@ -177,15 +181,14 @@ class BookController extends Controller
/** /**
* Saves an array of sort mapping to pages and chapters. * Saves an array of sort mapping to pages and chapters.
*
* @param string $bookSlug * @param string $bookSlug
* @param Request $request * @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/ */
public function saveSort($bookSlug, Request $request) public function saveSort($bookSlug, Request $request)
{ {
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent // Return if no map sent
if (!$request->has('sort-tree')) { if (!$request->has('sort-tree')) {
@ -223,17 +226,48 @@ class BookController extends Controller
/** /**
* Remove the specified book from storage. * Remove the specified book from storage.
*
* @param $bookSlug * @param $bookSlug
* @return Response * @return Response
*/ */
public function destroy($bookSlug) public function destroy($bookSlug)
{ {
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name); Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book); Activity::removeEntity($book);
$this->bookRepo->destroyBySlug($bookSlug); $this->bookRepo->destroyBySlug($bookSlug);
return redirect('/books'); return redirect('/books');
} }
/**
* Show the Restrictions view.
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
'book' => $book,
'roles' => $roles
]);
}
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
session()->flash('success', 'Page Restrictions Updated');
return redirect($book->getUrl());
}
} }

View File

@ -1,13 +1,9 @@
<?php <?php namespace BookStack\Http\Controllers;
namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests; use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo; use BookStack\Repos\ChapterRepo;
use Views; use Views;
@ -17,20 +13,22 @@ class ChapterController extends Controller
protected $bookRepo; protected $bookRepo;
protected $chapterRepo; protected $chapterRepo;
protected $userRepo;
/** /**
* ChapterController constructor. * ChapterController constructor.
* @param $bookRepo * @param BookRepo $bookRepo
* @param $chapterRepo * @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo
*/ */
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo) public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{ {
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo; $this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
} }
/** /**
* Show the form for creating a new chapter. * Show the form for creating a new chapter.
* @param $bookSlug * @param $bookSlug
@ -38,8 +36,8 @@ class ChapterController extends Controller
*/ */
public function create($bookSlug) public function create($bookSlug)
{ {
$this->checkPermission('chapter-create');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle('Create New Chapter'); $this->setPageTitle('Create New Chapter');
return view('chapters/create', ['book' => $book, 'current' => $book]); return view('chapters/create', ['book' => $book, 'current' => $book]);
} }
@ -52,12 +50,13 @@ class ChapterController extends Controller
*/ */
public function store($bookSlug, Request $request) public function store($bookSlug, Request $request)
{ {
$this->checkPermission('chapter-create');
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->newFromInput($request->all()); $chapter = $this->chapterRepo->newFromInput($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id); $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
$chapter->priority = $this->bookRepo->getNewPriority($book); $chapter->priority = $this->bookRepo->getNewPriority($book);
@ -81,7 +80,14 @@ class ChapterController extends Controller
$sidebarTree = $this->bookRepo->getChildren($book); $sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter); Views::add($chapter);
$this->setPageTitle($chapter->getShortName()); $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
]);
} }
/** /**
@ -92,9 +98,9 @@ class ChapterController extends Controller
*/ */
public function edit($bookSlug, $chapterSlug) public function edit($bookSlug, $chapterSlug)
{ {
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle('Edit Chapter' . $chapter->getShortName()); $this->setPageTitle('Edit Chapter' . $chapter->getShortName());
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
} }
@ -108,9 +114,9 @@ class ChapterController extends Controller
*/ */
public function update(Request $request, $bookSlug, $chapterSlug) public function update(Request $request, $bookSlug, $chapterSlug)
{ {
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$chapter->fill($request->all()); $chapter->fill($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id); $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
$chapter->updated_by = auth()->user()->id; $chapter->updated_by = auth()->user()->id;
@ -127,9 +133,9 @@ class ChapterController extends Controller
*/ */
public function showDelete($bookSlug, $chapterSlug) public function showDelete($bookSlug, $chapterSlug)
{ {
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle('Delete Chapter' . $chapter->getShortName()); $this->setPageTitle('Delete Chapter' . $chapter->getShortName());
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
} }
@ -142,11 +148,46 @@ class ChapterController extends Controller
*/ */
public function destroy($bookSlug, $chapterSlug) public function destroy($bookSlug, $chapterSlug)
{ {
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter);
Activity::addMessage('chapter_delete', $book->id, $chapter->name); Activity::addMessage('chapter_delete', $book->id, $chapter->name);
$this->chapterRepo->destroy($chapter); $this->chapterRepo->destroy($chapter);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
'chapter' => $chapter,
'roles' => $roles
]);
}
/**
* Set the restrictions for this chapter.
* @param $bookSlug
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
session()->flash('success', 'Page Restrictions Updated');
return redirect($chapter->getUrl());
}
} }

View File

@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use BookStack\Ownable;
use HttpRequestException; use HttpRequestException;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException; use Illuminate\Http\Exception\HttpResponseException;
@ -61,21 +62,19 @@ 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. * the error as a notification.
*/ */
protected function showPermissionError() protected function showPermissionError()
{ {
Session::flash('error', trans('errors.permission')); Session::flash('error', trans('errors.permission'));
throw new HttpResponseException( $response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
redirect('/') throw new HttpResponseException($response);
);
} }
/** /**
* Checks for a permission. * Checks for a permission.
* * @param string $permissionName
* @param $permissionName
* @return bool|\Illuminate\Http\RedirectResponse * @return bool|\Illuminate\Http\RedirectResponse
*/ */
protected function checkPermission($permissionName) protected function checkPermission($permissionName)
@ -83,10 +82,21 @@ abstract class Controller extends BaseController
if (!$this->currentUser || !$this->currentUser->can($permissionName)) { if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
$this->showPermissionError(); $this->showPermissionError();
} }
return true; 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)
{
if (userCan($permission, $ownable)) return true;
return $this->showPermissionError();
}
/** /**
* Check if a user has a permission or bypass if the callback is true. * Check if a user has a permission or bypass if the callback is true.
* @param $permissionName * @param $permissionName

View File

@ -24,7 +24,6 @@ class HomeController extends Controller
/** /**
* Display the homepage. * Display the homepage.
*
* @return Response * @return Response
*/ */
public function index() public function index()

View File

@ -64,7 +64,7 @@ class ImageController extends Controller
*/ */
public function uploadByType($type, Request $request) public function uploadByType($type, Request $request)
{ {
$this->checkPermission('image-create'); $this->checkPermission('image-create-all');
$this->validate($request, [ $this->validate($request, [
'file' => 'image|mimes:jpeg,gif,png' 'file' => 'image|mimes:jpeg,gif,png'
]); ]);
@ -90,7 +90,7 @@ class ImageController extends Controller
*/ */
public function getThumbnail($id, $width, $height, $crop) public function getThumbnail($id, $width, $height, $crop)
{ {
$this->checkPermission('image-create'); $this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id); $image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false'); $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]); return response()->json(['url' => $thumbnailUrl]);
@ -104,11 +104,11 @@ class ImageController extends Controller
*/ */
public function update($imageId, Request $request) public function update($imageId, Request $request)
{ {
$this->checkPermission('image-update');
$this->validate($request, [ $this->validate($request, [
'name' => 'required|min:2|string' 'name' => 'required|min:2|string'
]); ]);
$image = $this->imageRepo->getById($imageId); $image = $this->imageRepo->getById($imageId);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all()); $image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image); return response()->json($image);
} }
@ -123,8 +123,8 @@ class ImageController extends Controller
*/ */
public function destroy(PageRepo $pageRepo, Request $request, $id) public function destroy(PageRepo $pageRepo, Request $request, $id)
{ {
$this->checkPermission('image-delete');
$image = $this->imageRepo->getById($id); $image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages // Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);

View File

@ -1,12 +1,10 @@
<?php <?php namespace BookStack\Http\Controllers;
namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService; use BookStack\Services\ExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests; use BookStack\Http\Requests;
use BookStack\Repos\BookRepo; use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo; use BookStack\Repos\ChapterRepo;
@ -21,26 +19,28 @@ class PageController extends Controller
protected $bookRepo; protected $bookRepo;
protected $chapterRepo; protected $chapterRepo;
protected $exportService; protected $exportService;
protected $userRepo;
/** /**
* PageController constructor. * PageController constructor.
* @param PageRepo $pageRepo * @param PageRepo $pageRepo
* @param BookRepo $bookRepo * @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo * @param ChapterRepo $chapterRepo
* @param ExportService $exportService * @param ExportService $exportService
* @param UserRepo $userRepo
*/ */
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService) public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
{ {
$this->pageRepo = $pageRepo; $this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo; $this->chapterRepo = $chapterRepo;
$this->exportService = $exportService; $this->exportService = $exportService;
$this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
} }
/** /**
* Show the form for creating a new page. * Show the form for creating a new page.
*
* @param $bookSlug * @param $bookSlug
* @param bool $chapterSlug * @param bool $chapterSlug
* @return Response * @return Response
@ -48,23 +48,22 @@ class PageController extends Controller
*/ */
public function create($bookSlug, $chapterSlug = false) public function create($bookSlug, $chapterSlug = false)
{ {
$this->checkPermission('page-create');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false; $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$this->setPageTitle('Create New Page'); $this->setPageTitle('Create New Page');
return view('pages/create', ['book' => $book, 'chapter' => $chapter]); return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
} }
/** /**
* Store a newly created page in storage. * Store a newly created page in storage.
*
* @param Request $request * @param Request $request
* @param $bookSlug * @param $bookSlug
* @return Response * @return Response
*/ */
public function store(Request $request, $bookSlug) public function store(Request $request, $bookSlug)
{ {
$this->checkPermission('page-create');
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
@ -72,6 +71,8 @@ class PageController extends Controller
$input = $request->all(); $input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null; $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); $input['priority'] = $this->bookRepo->getNewPriority($book);
$page = $this->pageRepo->saveNew($input, $book, $chapterId); $page = $this->pageRepo->saveNew($input, $book, $chapterId);
@ -84,7 +85,6 @@ class PageController extends Controller
* Display the specified page. * Display the specified page.
* If the page is not found via the slug the * If the page is not found via the slug the
* revisions are searched for a match. * revisions are searched for a match.
*
* @param $bookSlug * @param $bookSlug
* @param $pageSlug * @param $pageSlug
* @return Response * @return Response
@ -95,7 +95,7 @@ class PageController extends Controller
try { try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
} catch (NotFoundHttpException $e) { } catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug); $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404); if ($page === null) abort(404);
return redirect($page->getUrl()); return redirect($page->getUrl());
@ -109,23 +109,21 @@ class PageController extends Controller
/** /**
* Show the form for editing the specified page. * Show the form for editing the specified page.
*
* @param $bookSlug * @param $bookSlug
* @param $pageSlug * @param $pageSlug
* @return Response * @return Response
*/ */
public function edit($bookSlug, $pageSlug) public function edit($bookSlug, $pageSlug)
{ {
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Editing Page ' . $page->getShortName()); $this->setPageTitle('Editing Page ' . $page->getShortName());
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
} }
/** /**
* Update the specified page in storage. * Update the specified page in storage.
*
* @param Request $request * @param Request $request
* @param $bookSlug * @param $bookSlug
* @param $pageSlug * @param $pageSlug
@ -133,12 +131,12 @@ class PageController extends Controller
*/ */
public function update(Request $request, $bookSlug, $pageSlug) public function update(Request $request, $bookSlug, $pageSlug)
{ {
$this->checkPermission('page-update');
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->updatePage($page, $book->id, $request->all()); $this->pageRepo->updatePage($page, $book->id, $request->all());
Activity::add($page, 'page_update', $book->id); Activity::add($page, 'page_update', $book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
@ -164,9 +162,9 @@ class PageController extends Controller
*/ */
public function showDelete($bookSlug, $pageSlug) public function showDelete($bookSlug, $pageSlug)
{ {
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle('Delete Page ' . $page->getShortName()); $this->setPageTitle('Delete Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
} }
@ -181,9 +179,9 @@ class PageController extends Controller
*/ */
public function destroy($bookSlug, $pageSlug) public function destroy($bookSlug, $pageSlug)
{ {
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name); Activity::addMessage('page_delete', $book->id, $page->name);
$this->pageRepo->destroy($page); $this->pageRepo->destroy($page);
return redirect($book->getUrl()); return redirect($book->getUrl());
@ -229,9 +227,9 @@ class PageController extends Controller
*/ */
public function restoreRevision($bookSlug, $pageSlug, $revisionId) public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{ {
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId); $page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
Activity::add($page, 'page_restore', $book->id); Activity::add($page, 'page_restore', $book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
@ -315,4 +313,39 @@ class PageController extends Controller
]); ]);
} }
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Set the restrictions for this page.
* @param $bookSlug
* @param $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->pageRepo->updateRestrictionsFromRequest($request, $page);
session()->flash('success', 'Page Restrictions Updated');
return redirect($page->getUrl());
}
} }

View File

@ -0,0 +1,129 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
class PermissionController extends Controller
{
protected $permissionsRepo;
/**
* PermissionController constructor.
* @param PermissionsRepo $permissionsRepo
*/
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
parent::__construct();
}
/**
* Show a listing of the roles in the system.
*/
public function listRoles()
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
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'
]);
$this->permissionsRepo->saveNewRole($request->all());
session()->flash('success', 'Role successfully created');
return redirect('/settings/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('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($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'
]);
$this->permissionsRepo->updateRole($id, $request->all());
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->permissionsRepo->getRoleById($id);
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $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');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
session()->flash('error', $e->getMessage());
return redirect()->back();
}
session()->flash('success', 'Role successfully deleted');
return redirect('/settings/roles');
}
}

View File

@ -17,7 +17,7 @@ class SettingController extends Controller
*/ */
public function index() public function index()
{ {
$this->checkPermission('settings-update'); $this->checkPermission('settings-manage');
$this->setPageTitle('Settings'); $this->setPageTitle('Settings');
return view('settings/index'); return view('settings/index');
} }
@ -32,7 +32,7 @@ class SettingController extends Controller
public function update(Request $request) public function update(Request $request)
{ {
$this->preventAccessForDemoUsers(); $this->preventAccessForDemoUsers();
$this->checkPermission('settings-update'); $this->checkPermission('settings-manage');
// Cycles through posted settings and update them // Cycles through posted settings and update them
foreach($request->all() as $name => $value) { foreach($request->all() as $name => $value) {

View File

@ -35,7 +35,8 @@ class UserController extends Controller
*/ */
public function index() public function index()
{ {
$users = $this->user->all(); $this->checkPermission('users-manage');
$users = $this->userRepo->getAllUsers();
$this->setPageTitle('Users'); $this->setPageTitle('Users');
return view('users/index', ['users' => $users]); return view('users/index', ['users' => $users]);
} }
@ -46,7 +47,7 @@ class UserController extends Controller
*/ */
public function create() public function create()
{ {
$this->checkPermission('user-create'); $this->checkPermission('users-manage');
$authMethod = config('auth.method'); $authMethod = config('auth.method');
return view('users/create', ['authMethod' => $authMethod]); return view('users/create', ['authMethod' => $authMethod]);
} }
@ -58,11 +59,10 @@ class UserController extends Controller
*/ */
public function store(Request $request) public function store(Request $request)
{ {
$this->checkPermission('user-create'); $this->checkPermission('users-manage');
$validationRules = [ $validationRules = [
'name' => 'required', 'name' => 'required',
'email' => 'required|email|unique:users,email', 'email' => 'required|email|unique:users,email'
'role' => 'required|exists:roles,id'
]; ];
$authMethod = config('auth.method'); $authMethod = config('auth.method');
@ -84,7 +84,11 @@ class UserController extends Controller
} }
$user->save(); $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 // Get avatar from gravatar and save
if (!config('services.disable_services')) { if (!config('services.disable_services')) {
@ -104,7 +108,7 @@ class UserController extends Controller
*/ */
public function edit($id, SocialAuthService $socialAuthService) 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; return $this->currentUser->id == $id;
}); });
@ -125,7 +129,7 @@ class UserController extends Controller
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$this->preventAccessForDemoUsers(); $this->preventAccessForDemoUsers();
$this->checkPermissionOr('user-update', function () use ($id) { $this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id; return $this->currentUser->id == $id;
}); });
@ -133,8 +137,7 @@ class UserController extends Controller
'name' => 'min:2', 'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id, 'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm', 'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password', 'password-confirm' => 'same:password|required_with:password'
'role' => 'exists:roles,id'
], [ ], [
'password-confirm.required_with' => 'Password confirmation required' 'password-confirm.required_with' => 'Password confirmation required'
]); ]);
@ -143,8 +146,9 @@ class UserController extends Controller
$user->fill($request->all()); $user->fill($request->all());
// Role updates // Role updates
if ($this->currentUser->can('user-update') && $request->has('role')) { if (userCan('users-manage') && $request->has('roles')) {
$user->attachRoleId($request->get('role')); $roles = $request->get('roles');
$user->roles()->sync($roles);
} }
// Password updates // Password updates
@ -154,11 +158,12 @@ class UserController extends Controller
} }
// External auth id updates // 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->external_auth_id = $request->get('external_auth_id');
} }
$user->save(); $user->save();
session()->flash('success', 'User successfully updated');
return redirect('/settings/users'); return redirect('/settings/users');
} }
@ -169,7 +174,7 @@ class UserController extends Controller
*/ */
public function delete($id) public function delete($id)
{ {
$this->checkPermissionOr('user-delete', function () use ($id) { $this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id; return $this->currentUser->id == $id;
}); });
@ -186,7 +191,7 @@ class UserController extends Controller
public function destroy($id) public function destroy($id)
{ {
$this->preventAccessForDemoUsers(); $this->preventAccessForDemoUsers();
$this->checkPermissionOr('user-delete', function () use ($id) { $this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id; return $this->currentUser->id == $id;
}); });

View File

@ -19,6 +19,8 @@ Route::group(['middleware' => 'auth'], function () {
Route::delete('/{id}', 'BookController@destroy'); Route::delete('/{id}', 'BookController@destroy');
Route::get('/{slug}/sort-item', 'BookController@getSortItem'); Route::get('/{slug}/sort-item', 'BookController@getSortItem');
Route::get('/{slug}', 'BookController@show'); Route::get('/{slug}', 'BookController@show');
Route::get('/{bookSlug}/restrict', 'BookController@showRestrict');
Route::put('/{bookSlug}/restrict', 'BookController@restrict');
Route::get('/{slug}/delete', 'BookController@showDelete'); Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort'); Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort'); Route::put('/{bookSlug}/sort', 'BookController@saveSort');
@ -32,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict');
Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy'); Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
@ -47,6 +51,8 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update'); Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit'); Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
Route::get('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@showRestrict');
Route::put('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@restrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete'); Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy'); Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
@ -87,6 +93,7 @@ Route::group(['middleware' => 'auth'], function () {
Route::group(['prefix' => 'settings'], function() { Route::group(['prefix' => 'settings'], function() {
Route::get('/', 'SettingController@index'); Route::get('/', 'SettingController@index');
Route::post('/', 'SettingController@update'); Route::post('/', 'SettingController@update');
// Users // Users
Route::get('/users', 'UserController@index'); Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create'); Route::get('/users/create', 'UserController@create');
@ -95,6 +102,15 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/users/{id}', 'UserController@edit'); Route::get('/users/{id}', 'UserController@edit');
Route::put('/users/{id}', 'UserController@update'); Route::put('/users/{id}', 'UserController@update');
Route::delete('/users/{id}', 'UserController@destroy'); Route::delete('/users/{id}', 'UserController@destroy');
// 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');
}); });
}); });

View File

@ -1,14 +1,9 @@
<?php <?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
use Images; use Images;
class Image extends Model class Image extends Ownable
{ {
use Ownable;
protected $fillable = ['name']; protected $fillable = ['name'];

View File

@ -1,7 +1,8 @@
<?php namespace BookStack; <?php namespace BookStack;
use Illuminate\Database\Eloquent\Model;
trait Ownable abstract class Ownable extends Model
{ {
/** /**
* Relation for the user that created this entity. * Relation for the user that created this entity.
@ -20,4 +21,14 @@ trait Ownable
{ {
return $this->belongsTo('BookStack\User', 'updated_by'); return $this->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]);
}
} }

View File

@ -13,4 +13,14 @@ class Permission extends Model
{ {
return $this->belongsToMany('BookStack\Permissions'); 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();
}
} }

View File

@ -28,11 +28,17 @@ class CustomFacadeProvider extends ServiceProvider
public function register() public function register()
{ {
$this->app->bind('activity', function() { $this->app->bind('activity', function() {
return new ActivityService($this->app->make('BookStack\Activity')); return new ActivityService(
$this->app->make('BookStack\Activity'),
$this->app->make('BookStack\Services\RestrictionService')
);
}); });
$this->app->bind('views', function() { $this->app->bind('views', function() {
return new ViewService($this->app->make('BookStack\View')); return new ViewService(
$this->app->make('BookStack\View'),
$this->app->make('BookStack\Services\RestrictionService')
);
}); });
$this->app->bind('setting', function() { $this->app->bind('setting', function() {
@ -41,6 +47,7 @@ class CustomFacadeProvider extends ServiceProvider
$this->app->make('Illuminate\Contracts\Cache\Repository') $this->app->make('Illuminate\Contracts\Cache\Repository')
); );
}); });
$this->app->bind('images', function() { $this->app->bind('images', function() {
return new ImageService( return new ImageService(
$this->app->make('Intervention\Image\ImageManager'), $this->app->make('Intervention\Image\ImageManager'),

View File

@ -1,28 +1,35 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Repos;
use Activity; use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Book; use BookStack\Book;
use Views; use Views;
class BookRepo class BookRepo extends EntityRepo
{ {
protected $book;
protected $pageRepo; protected $pageRepo;
protected $chapterRepo; protected $chapterRepo;
/** /**
* BookRepo constructor. * BookRepo constructor.
* @param Book $book
* @param PageRepo $pageRepo * @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo * @param ChapterRepo $chapterRepo
*/ */
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo) public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
{ {
$this->book = $book;
$this->pageRepo = $pageRepo; $this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo; $this->chapterRepo = $chapterRepo;
parent::__construct();
}
/**
* Base query for getting books.
* Takes into account any restrictions.
* @return mixed
*/
private function bookQuery()
{
return $this->restrictionService->enforceBookRestrictions($this->book, 'view');
} }
/** /**
@ -32,7 +39,7 @@ class BookRepo
*/ */
public function getById($id) public function getById($id)
{ {
return $this->book->findOrFail($id); return $this->bookQuery()->findOrFail($id);
} }
/** /**
@ -42,7 +49,7 @@ class BookRepo
*/ */
public function getAll($count = 10) public function getAll($count = 10)
{ {
$bookQuery = $this->book->orderBy('name', 'asc'); $bookQuery = $this->bookQuery()->orderBy('name', 'asc');
if (!$count) return $bookQuery->get(); if (!$count) return $bookQuery->get();
return $bookQuery->take($count)->get(); return $bookQuery->take($count)->get();
} }
@ -54,7 +61,8 @@ class BookRepo
*/ */
public function getAllPaginated($count = 10) public function getAllPaginated($count = 10)
{ {
return $this->book->orderBy('name', 'asc')->paginate($count); return $this->bookQuery()
->orderBy('name', 'asc')->paginate($count);
} }
@ -65,7 +73,7 @@ class BookRepo
*/ */
public function getLatest($count = 10) public function getLatest($count = 10)
{ {
return $this->book->orderBy('created_at', 'desc')->take($count)->get(); return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
} }
/** /**
@ -94,11 +102,12 @@ class BookRepo
* Get a book by slug * Get a book by slug
* @param $slug * @param $slug
* @return mixed * @return mixed
* @throws NotFoundException
*/ */
public function getBySlug($slug) public function getBySlug($slug)
{ {
$book = $this->book->where('slug', '=', $slug)->first(); $book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) abort(404); if ($book === null) throw new NotFoundException('Book not found');
return $book; return $book;
} }
@ -109,7 +118,7 @@ class BookRepo
*/ */
public function exists($id) public function exists($id)
{ {
return $this->book->where('id', '=', $id)->exists(); return $this->bookQuery()->where('id', '=', $id)->exists();
} }
/** /**
@ -119,17 +128,7 @@ class BookRepo
*/ */
public function newFromInput($input) public function newFromInput($input)
{ {
return $this->book->fill($input); return $this->book->newInstance($input);
}
/**
* Count the amount of books that have a specific slug.
* @param $slug
* @return mixed
*/
public function countBySlug($slug)
{
return $this->book->where('slug', '=', $slug)->count();
} }
/** /**
@ -146,6 +145,7 @@ class BookRepo
$this->chapterRepo->destroy($chapter); $this->chapterRepo->destroy($chapter);
} }
$book->views()->delete(); $book->views()->delete();
$book->restrictions()->delete();
$book->delete(); $book->delete();
} }
@ -202,8 +202,15 @@ class BookRepo
*/ */
public function getChildren(Book $book) public function getChildren(Book $book)
{ {
$pages = $book->pages()->where('chapter_id', '=', 0)->get(); $pageQuery = $book->pages()->where('chapter_id', '=', 0);
$chapters = $book->chapters()->with('pages')->get(); $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); $children = $pages->merge($chapters);
$bookSlug = $book->slug; $bookSlug = $book->slug;
$children->each(function ($child) use ($bookSlug) { $children->each(function ($child) use ($bookSlug) {
@ -226,8 +233,8 @@ class BookRepo
*/ */
public function getBySearch($term, $count = 20, $paginationAppends = []) public function getBySearch($term, $count = 20, $paginationAppends = [])
{ {
$terms = explode(' ', $term); $terms = $this->prepareSearchTerms($term);
$books = $this->book->fullTextSearchQuery(['name', 'description'], $terms) $books = $this->restrictionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
->paginate($count)->appends($paginationAppends); ->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/'))); $words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) { foreach ($books as $book) {

View File

@ -2,21 +2,19 @@
use Activity; use Activity;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Chapter; use BookStack\Chapter;
class ChapterRepo class ChapterRepo extends EntityRepo
{ {
protected $chapter;
/** /**
* ChapterRepo constructor. * Base query for getting chapters, Takes restrictions into account.
* @param $chapter * @return mixed
*/ */
public function __construct(Chapter $chapter) private function chapterQuery()
{ {
$this->chapter = $chapter; return $this->restrictionService->enforceChapterRestrictions($this->chapter, 'view');
} }
/** /**
@ -26,7 +24,7 @@ class ChapterRepo
*/ */
public function idExists($id) public function idExists($id)
{ {
return $this->chapter->where('id', '=', $id)->count() > 0; return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
} }
/** /**
@ -36,7 +34,7 @@ class ChapterRepo
*/ */
public function getById($id) public function getById($id)
{ {
return $this->chapter->findOrFail($id); return $this->chapterQuery()->findOrFail($id);
} }
/** /**
@ -45,7 +43,7 @@ class ChapterRepo
*/ */
public function getAll() public function getAll()
{ {
return $this->chapter->all(); return $this->chapterQuery()->all();
} }
/** /**
@ -53,14 +51,24 @@ class ChapterRepo
* @param $slug * @param $slug
* @param $bookId * @param $bookId
* @return mixed * @return mixed
* @throws NotFoundException
*/ */
public function getBySlug($slug, $bookId) public function getBySlug($slug, $bookId)
{ {
$chapter = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); $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; 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. * Create a new chapter from request input.
* @param $input * @param $input
@ -85,6 +93,7 @@ class ChapterRepo
} }
Activity::removeEntity($chapter); Activity::removeEntity($chapter);
$chapter->views()->delete(); $chapter->views()->delete();
$chapter->restrictions()->delete();
$chapter->delete(); $chapter->delete();
} }
@ -123,7 +132,7 @@ class ChapterRepo
/** /**
* Get chapters by the given search term. * Get chapters by the given search term.
* @param $term * @param string $term
* @param array $whereTerms * @param array $whereTerms
* @param int $count * @param int $count
* @param array $paginationAppends * @param array $paginationAppends
@ -131,8 +140,8 @@ class ChapterRepo
*/ */
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{ {
$terms = explode(' ', $term); $terms = $this->prepareSearchTerms($term);
$chapters = $this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms) $chapters = $this->restrictionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
->paginate($count)->appends($paginationAppends); ->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/'))); $words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) { foreach ($chapters as $chapter) {

View File

@ -1,28 +1,43 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Repos;
use BookStack\Book; use BookStack\Book;
use BookStack\Chapter; use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page; use BookStack\Page;
use BookStack\Services\RestrictionService;
class EntityRepo class EntityRepo
{ {
/**
* @var Book $book
*/
public $book; public $book;
/**
* @var Chapter
*/
public $chapter; public $chapter;
/**
* @var Page
*/
public $page; public $page;
/** /**
* EntityService constructor. * @var RestrictionService
* @param $book
* @param $chapter
* @param $page
*/ */
public function __construct(Book $book, Chapter $chapter, Page $page) protected $restrictionService;
/**
* EntityService constructor.
*/
public function __construct()
{ {
$this->book = $book; $this->book = app(Book::class);
$this->chapter = $chapter; $this->chapter = app(Chapter::class);
$this->page = $page; $this->page = app(Page::class);
$this->restrictionService = app(RestrictionService::class);
} }
/** /**
@ -32,7 +47,8 @@ class EntityRepo
*/ */
public function getRecentlyCreatedBooks($count = 20, $page = 0) public function getRecentlyCreatedBooks($count = 20, $page = 0)
{ {
return $this->book->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get(); return $this->restrictionService->enforceBookRestrictions($this->book)
->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get();
} }
/** /**
@ -43,7 +59,8 @@ class EntityRepo
*/ */
public function getRecentlyUpdatedBooks($count = 20, $page = 0) public function getRecentlyUpdatedBooks($count = 20, $page = 0)
{ {
return $this->book->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get(); return $this->restrictionService->enforceBookRestrictions($this->book)
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
} }
/** /**
@ -53,7 +70,8 @@ class EntityRepo
*/ */
public function getRecentlyCreatedPages($count = 20, $page = 0) public function getRecentlyCreatedPages($count = 20, $page = 0)
{ {
return $this->page->orderBy('created_at', 'desc')->skip($page*$count)->take($count)->get(); return $this->restrictionService->enforcePageRestrictions($this->page)
->orderBy('created_at', 'desc')->skip($page * $count)->take($count)->get();
} }
/** /**
@ -64,7 +82,50 @@ class EntityRepo
*/ */
public function getRecentlyUpdatedPages($count = 20, $page = 0) public function getRecentlyUpdatedPages($count = 20, $page = 0)
{ {
return $this->page->orderBy('updated_at', 'desc')->skip($page*$count)->take($count)->get(); return $this->restrictionService->enforcePageRestrictions($this->page)
->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;
} }

View File

@ -3,39 +3,32 @@
use Activity; use Activity;
use BookStack\Book; use BookStack\Book;
use BookStack\Chapter; use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Page; use BookStack\Page;
use BookStack\PageRevision; use BookStack\PageRevision;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class PageRepo class PageRepo extends EntityRepo
{ {
protected $page;
protected $pageRevision; protected $pageRevision;
/** /**
* PageRepo constructor. * PageRepo constructor.
* @param Page $page
* @param PageRevision $pageRevision * @param PageRevision $pageRevision
*/ */
public function __construct(Page $page, PageRevision $pageRevision) public function __construct(PageRevision $pageRevision)
{ {
$this->page = $page;
$this->pageRevision = $pageRevision; $this->pageRevision = $pageRevision;
parent::__construct();
} }
/** /**
* Check if a page id exists. * Base query for getting pages, Takes restrictions into account.
* @param $id * @return mixed
* @return bool
*/ */
public function idExists($id) private function pageQuery()
{ {
return $this->page->where('page_id', '=', $id)->count() > 0; return $this->restrictionService->enforcePageRestrictions($this->page, 'view');
} }
/** /**
@ -45,16 +38,7 @@ class PageRepo
*/ */
public function getById($id) public function getById($id)
{ {
return $this->page->findOrFail($id); return $this->pageQuery()->findOrFail($id);
}
/**
* Get all pages.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->page->all();
} }
/** /**
@ -62,11 +46,12 @@ class PageRepo
* @param $slug * @param $slug
* @param $bookId * @param $bookId
* @return mixed * @return mixed
* @throws NotFoundException
*/ */
public function getBySlug($slug, $bookId) public function getBySlug($slug, $bookId)
{ {
$page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); $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; return $page;
} }
@ -81,6 +66,9 @@ class PageRepo
public function findPageUsingOldSlug($pageSlug, $bookSlug) public function findPageUsingOldSlug($pageSlug, $bookSlug)
{ {
$revision = $this->pageRevision->where('slug', '=', $pageSlug) $revision = $this->pageRevision->where('slug', '=', $pageSlug)
->whereHas('page', function($query) {
$this->restrictionService->enforcePageRestrictions($query);
})
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
->with('page')->first(); ->with('page')->first();
return $revision !== null ? $revision->page : null; return $revision !== null ? $revision->page : null;
@ -201,8 +189,8 @@ class PageRepo
*/ */
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{ {
$terms = explode(' ', $term); $terms = $this->prepareSearchTerms($term);
$pages = $this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms) $pages = $this->restrictionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
->paginate($count)->appends($paginationAppends); ->paginate($count)->appends($paginationAppends);
// Add highlights to page text. // Add highlights to page text.
@ -240,7 +228,7 @@ class PageRepo
*/ */
public function searchForImage($imageString) public function searchForImage($imageString)
{ {
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get(); $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) { foreach ($pages as $page) {
$page->url = $page->getUrl(); $page->url = $page->getUrl();
$page->html = ''; $page->html = '';
@ -386,6 +374,7 @@ class PageRepo
Activity::removeEntity($page); Activity::removeEntity($page);
$page->views()->delete(); $page->views()->delete();
$page->revisions()->delete(); $page->revisions()->delete();
$page->restrictions()->delete();
$page->delete(); $page->delete();
} }
@ -395,7 +384,7 @@ class PageRepo
*/ */
public function getRecentlyCreatedPaginated($count = 20) public function getRecentlyCreatedPaginated($count = 20)
{ {
return $this->page->orderBy('created_at', 'desc')->paginate($count); return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
} }
/** /**
@ -404,7 +393,7 @@ class PageRepo
*/ */
public function getRecentlyUpdatedPaginated($count = 20) public function getRecentlyUpdatedPaginated($count = 20)
{ {
return $this->page->orderBy('updated_at', 'desc')->paginate($count); return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
} }
} }

View File

@ -0,0 +1,142 @@
<?php namespace BookStack\Repos;
use BookStack\Exceptions\PermissionsException;
use BookStack\Permission;
use BookStack\Role;
use Setting;
class PermissionsRepo
{
protected $permission;
protected $role;
/**
* PermissionsRepo constructor.
* @param $permission
* @param $role
*/
public function __construct(Permission $permission, Role $role)
{
$this->permission = $permission;
$this->role = $role;
}
/**
* Get all the user roles from the system.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAllRoles()
{
return $this->role->all();
}
/**
* Get all the roles except for the provided one.
* @param Role $role
* @return mixed
*/
public function getAllRolesExcept(Role $role)
{
return $this->role->where('id', '!=', $role->id)->get();
}
/**
* Get a role via its ID.
* @param $id
* @return mixed
*/
public function getRoleById($id)
{
return $this->role->findOrFail($id);
}
/**
* Save a new role into the system.
* @param array $roleData
* @return Role
*/
public function saveNewRole($roleData)
{
$role = $this->role->newInstance($roleData);
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
$role->name .= strtolower(str_random(2));
}
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
return $role;
}
/**
* Updates an existing role.
* Ensure Admin role always has all permissions.
* @param $roleId
* @param $roleData
*/
public function updateRole($roleId, $roleData)
{
$role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
}
$role->fill($roleData);
$role->save();
}
/**
* Assign an list of permission names to an role.
* @param Role $role
* @param array $permissionNameArray
*/
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();
}
$role->permissions()->sync($permissions);
}
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
* @param $roleId
* @param $migrateRoleId
* @throws PermissionsException
*/
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->role->findOrFail($roleId);
// 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')) {
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
}
if ($migrateRoleId) {
$newRole = $this->role->find($migrateRoleId);
if ($newRole) {
$users = $role->users->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}
$role->delete();
}
}

View File

@ -42,6 +42,15 @@ class UserRepo
return $this->user->findOrFail($id); 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. * Creates a new user and attaches a role to them.
* @param array $data * @param array $data
@ -69,7 +78,7 @@ class UserRepo
public function attachDefaultRole($user) public function attachDefaultRole($user)
{ {
$roleId = Setting::get('registration-role'); $roleId = Setting::get('registration-role');
if ($roleId === false) $roleId = $this->role->getDefault()->id; if ($roleId === false) $roleId = $this->role->first()->id;
$user->attachRoleId($roleId); $user->attachRoleId($roleId);
} }
@ -80,15 +89,10 @@ class UserRepo
*/ */
public function isOnlyAdmin(User $user) public function isOnlyAdmin(User $user)
{ {
if ($user->role->name != 'admin') { if (!$user->roles->pluck('name')->contains('admin')) return false;
return false;
}
$adminRole = $this->role->where('name', '=', 'admin')->first();
if (count($adminRole->users) > 1) {
return false;
}
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
return true; return true;
} }
@ -160,4 +164,14 @@ class UserRepo
]; ];
} }
/**
* Get all the roles which can be given restricted access to
* other entities in the system.
* @return mixed
*/
public function getRestrictableRoles()
{
return $this->role->where('name', '!=', 'admin')->get();
}
} }

21
app/Restriction.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class Restriction extends Model
{
protected $fillable = ['role_id', 'action'];
public $timestamps = false;
/**
* Get all this restriction's attached entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function restrictable()
{
return $this->morphTo();
}
}

View File

@ -6,11 +6,8 @@ use Illuminate\Database\Eloquent\Model;
class Role extends Model class Role extends Model
{ {
/**
* Sets the default role name for newly registered users. protected $fillable = ['display_name', 'description'];
* @var string
*/
protected static $default = 'viewer';
/** /**
* The roles that belong to the role. * The roles that belong to the role.
@ -28,6 +25,15 @@ class Role extends Model
return $this->belongsToMany('BookStack\Permission'); 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. * Add a permission to this role.
* @param Permission $permission * @param Permission $permission
@ -37,15 +43,6 @@ class Role extends Model
$this->permissions()->attach($permission->id); $this->permissions()->attach($permission->id);
} }
/**
* Get an instance of the default role.
* @return Role
*/
public static function getDefault()
{
return static::getRole(static::$default);
}
/** /**
* Get the role object for the specified role. * Get the role object for the specified role.
* @param $roleName * @param $roleName

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Services;
use Illuminate\Support\Facades\Auth;
use BookStack\Activity; use BookStack\Activity;
use BookStack\Entity; use BookStack\Entity;
use Session; use Session;
@ -9,14 +8,17 @@ class ActivityService
{ {
protected $activity; protected $activity;
protected $user; protected $user;
protected $restrictionService;
/** /**
* ActivityService constructor. * ActivityService constructor.
* @param $activity * @param Activity $activity
* @param RestrictionService $restrictionService
*/ */
public function __construct(Activity $activity) public function __construct(Activity $activity, RestrictionService $restrictionService)
{ {
$this->activity = $activity; $this->activity = $activity;
$this->restrictionService = $restrictionService;
$this->user = auth()->user(); $this->user = auth()->user();
} }
@ -86,8 +88,10 @@ class ActivityService
*/ */
public function latest($count = 20, $page = 0) public function latest($count = 20, $page = 0)
{ {
$activityList = $this->activity->orderBy('created_at', 'desc') $activityList = $this->restrictionService
->skip($count * $page)->take($count)->get(); ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activityList); return $this->filterSimilar($activityList);
} }

View File

@ -0,0 +1,272 @@
<?php namespace BookStack\Services;
use BookStack\Entity;
class RestrictionService
{
protected $userRoles;
protected $isAdmin;
protected $currentAction;
/**
* RestrictionService constructor.
*/
public function __construct()
{
$user = auth()->user();
$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;
$this->currentAction = $action;
$baseQuery = $entity->where('id', '=', $entity->id);
if ($entity->isA('page')) {
return $this->pageRestrictionQuery($baseQuery)->count() > 0;
} elseif ($entity->isA('chapter')) {
return $this->chapterRestrictionQuery($baseQuery)->count() > 0;
} elseif ($entity->isA('book')) {
return $this->bookRestrictionQuery($baseQuery)->count() > 0;
}
return false;
}
/**
* Add restrictions for a page query
* @param $query
* @param string $action
* @return mixed
*/
public function enforcePageRestrictions($query, $action = 'view')
{
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->pageRestrictionQuery($query);
}
/**
* The base query for restricting pages.
* @param $query
* @return mixed
*/
private function pageRestrictionQuery($query)
{
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
// (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted
->where(function ($query) {
$query->where(function ($query) {
$query->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')
->where('restricted', '=', false);
})->where('restricted', '=', false);
})->orWhere(function ($query) {
$query->where('restricted', '=', false)->where('chapter_id', '=', 0)
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->where('restricted', '=', false);
});
});
})
// Page unrestricted, Has no 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');
}, 'and', true)
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
})
// 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');
});
});
})
// Page has accepted permissions
->orWhereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'pages', 'Page');
});
});
}
/**
* Add on permission restrictions to a chapter query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceChapterRestrictions($query, $action = 'view')
{
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->chapterRestrictionQuery($query);
}
/**
* The base query for restricting chapters.
* @param $query
* @return mixed
*/
private function chapterRestrictionQuery($query)
{
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
// Book & chapter unrestricted
->where(function ($query) {
$query->where('restricted', '=', false)->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=chapters.book_id')
->where('restricted', '=', false);
});
})
// Chapter unrestricted & book has accepted restrictions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=chapters.book_id')
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
})
// Chapter has accepted permissions
->orWhereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
});
});
}
/**
* Add restrictions to a book query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceBookRestrictions($query, $action = 'view')
{
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->bookRestrictionQuery($query);
}
/**
* The base query for restricting books.
* @param $query
* @return mixed
*/
private function bookRestrictionQuery($query)
{
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
->where('restricted', '=', false)
->orWhere(function ($query) {
$query->where('restricted', '=', true)->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
});
}
/**
* Filter items that have entities set a a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
{
if ($this->isAdmin) return $query;
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
return $query->where(function ($query) use ($tableDetails) {
$query->where(function ($query) use (&$tableDetails) {
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page')
->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->pageRestrictionQuery($query);
});
});
})->orWhere(function ($query) use (&$tableDetails) {
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->bookRestrictionQuery($query);
});
});
})->orWhere(function ($query) use (&$tableDetails) {
$query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
$query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where(function ($query) {
$this->chapterRestrictionQuery($query);
});
});
});
});
}
/**
* The query to check the restrictions on an entity.
* @param $query
* @param $tableName
* @param $modelName
*/
private function checkRestrictionsQuery($query, $tableName, $modelName)
{
$query->select('*')->from('restrictions')
->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
->where('restrictions.action', '=', $this->currentAction)
->whereIn('restrictions.role_id', $this->userRoles);
}
}

View File

@ -9,15 +9,18 @@ class ViewService
protected $view; protected $view;
protected $user; protected $user;
protected $restrictionService;
/** /**
* ViewService constructor. * ViewService constructor.
* @param $view * @param View $view
* @param RestrictionService $restrictionService
*/ */
public function __construct(View $view) public function __construct(View $view, RestrictionService $restrictionService)
{ {
$this->view = $view; $this->view = $view;
$this->user = auth()->user(); $this->user = auth()->user();
$this->restrictionService = $restrictionService;
} }
/** /**
@ -27,7 +30,7 @@ class ViewService
*/ */
public function add(Entity $entity) public function add(Entity $entity)
{ {
if($this->user === null) return 0; if ($this->user === null) return 0;
$view = $entity->views()->where('user_id', '=', $this->user->id)->first(); $view = $entity->views()->where('user_id', '=', $this->user->id)->first();
// Add view if model exists // Add view if model exists
if ($view) { if ($view) {
@ -47,18 +50,19 @@ class ViewService
/** /**
* Get the entities with the most views. * Get the entities with the most views.
* @param int $count * @param int $count
* @param int $page * @param int $page
* @param bool|false $filterModel * @param bool|false $filterModel
*/ */
public function getPopular($count = 10, $page = 0, $filterModel = false) public function getPopular($count = 10, $page = 0, $filterModel = false)
{ {
$skipCount = $count * $page; $skipCount = $count * $page;
$query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) $query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type') ->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc'); ->orderBy('view_count', 'desc');
if($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
$views = $query->with('viewable')->skip($skipCount)->take($count)->get(); $views = $query->with('viewable')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) { $viewedEntities = $views->map(function ($item) {
@ -69,22 +73,24 @@ class ViewService
/** /**
* Get all recently viewed entities for the current user. * Get all recently viewed entities for the current user.
* @param int $count * @param int $count
* @param int $page * @param int $page
* @param Entity|bool $filterModel * @param Entity|bool $filterModel
* @return mixed * @return mixed
*/ */
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{ {
if($this->user === null) return collect(); if ($this->user === null) return collect();
$skipCount = $count * $page; $skipCount = $count * $page;
$query = $this->view->where('user_id', '=', auth()->user()->id); $query = $this->restrictionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', auth()->user()->id);
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get(); $views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) { $viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults(); return $item->viewable;
}); });
return $viewedEntities; return $viewedEntities;
} }

View File

@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* The database table used by the model. * The database table used by the model.
*
* @var string * @var string
*/ */
protected $table = 'users'; protected $table = 'users';
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
*
* @var array * @var array
*/ */
protected $fillable = ['name', 'email', 'image_id']; protected $fillable = ['name', 'email', 'image_id'];
/** /**
* The attributes excluded from the model's JSON form. * The attributes excluded from the model's JSON form.
*
* @var array * @var array
*/ */
protected $hidden = ['password', 'remember_token']; 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. * The roles that belong to the user.
*/ */
@ -62,21 +55,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->belongsToMany('BookStack\Role'); 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.
* @param bool $cache
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/ */
private function loadPermissions() public function permissions($cache = true)
{ {
if (isset($this->permissions)) return; if(isset($this->permissions) && $cache) return $this->permissions;
$this->load('roles.permissions'); $this->load('roles.permissions');
$permissions = $this->roles[0]->permissions; $permissions = $this->roles->map(function($role) {
$permissionsArray = $permissions->pluck('name')->all(); return $role->permissions;
$this->permissions = $permissionsArray; })->flatten()->unique();
$this->permissions = $permissions;
return $permissions;
} }
/** /**
@ -86,11 +88,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/ */
public function can($permissionName) public function can($permissionName)
{ {
if ($this->email == 'guest') { if ($this->email === 'guest') return false;
return false; return $this->permissions()->pluck('name')->contains($permissionName);
}
$this->loadPermissions();
return array_search($permissionName, $this->permissions) !== false;
} }
/** /**
@ -108,12 +107,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/ */
public function attachRoleId($id) public function attachRoleId($id)
{ {
$this->roles()->sync([$id]); $this->roles()->attach($id);
} }
/** /**
* Get the social account associated with this user. * Get the social account associated with this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany * @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */
public function socialAccounts() public function socialAccounts()
@ -138,8 +136,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Returns the user's avatar, * Returns the user's avatar,
* Uses Gravatar as the avatar service.
*
* @param int $size * @param int $size
* @return string * @return string
*/ */

View File

@ -1,10 +1,10 @@
<?php <?php
if (! function_exists('versioned_asset')) { if (!function_exists('versioned_asset')) {
/** /**
* Get the path to a versioned file. * Get the path to a versioned file.
* *
* @param string $file * @param string $file
* @return string * @return string
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
@ -27,4 +27,35 @@ if (! function_exists('versioned_asset')) {
throw new InvalidArgumentException("File {$file} not defined in asset manifest."); 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 (!auth()->check()) return false;
if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission);
}
// Check permission on ownable item
$permissionBaseName = strtolower($permission) . '-';
$hasPermission = false;
if (auth()->user()->can($permissionBaseName . 'all')) $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;
// Check restrictions on the entitiy
$restrictionService = app('BookStack\Services\RestrictionService');
$explodedPermission = explode('-', $permission);
$action = end($explodedPermission);
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
return $hasAccess && $hasPermission;
} }

View File

@ -1,5 +1,18 @@
<?php <?php
// MEMCACHED - Split out configuration into an array
if (env('CACHE_DRIVER') === 'memcached') {
$memcachedServerKeys = ['host', 'port', 'weight'];
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
foreach ($memcachedServers as $index => $memcachedServer) {
$memcachedServerDetails = explode(':', $memcachedServer);
$components = count($memcachedServerDetails);
if ($components < 2) $memcachedServerDetails[] = '11211';
if ($components < 3) $memcachedServerDetails[] = '100';
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
}
}
return [ return [
/* /*
@ -49,11 +62,7 @@ return [
'memcached' => [ 'memcached' => [
'driver' => 'memcached', 'driver' => 'memcached',
'servers' => [ 'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [],
[
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
],
],
], ],
'redis' => [ 'redis' => [

View File

@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) {
'email' => $faker->email, 'email' => $faker->email,
'password' => str_random(10), 'password' => str_random(10),
'remember_token' => str_random(10), 'remember_token' => str_random(10),
'email_confirmed' => 1
]; ];
}); });
@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) {
'text' => strip_tags($html) 'text' => strip_tags($html)
]; ];
}); });
$factory->define(BookStack\Role::class, function ($faker) {
return [
'display_name' => $faker->sentence(3),
'description' => $faker->sentence(10)
];
});

View File

@ -0,0 +1,99 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdatePermissionsAndRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Get roles with permissions we need to change
$adminRole = \BookStack\Role::getRole('admin');
$editorRole = \BookStack\Role::getRole('editor');
// Delete old permissions
$permissions = \BookStack\Permission::all();
$permissions->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',
'restrictions-manage-all' => 'Manage All Entity Restrictions',
'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content'
];
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);
}
}
}
}

View File

@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEntityAccessControls extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('images', function (Blueprint $table) {
$table->integer('uploaded_to')->default(0);
$table->index('uploaded_to');
});
Schema::table('books', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::table('chapters', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::table('pages', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::create('restrictions', function(Blueprint $table) {
$table->increments('id');
$table->integer('restrictable_id');
$table->string('restrictable_type');
$table->integer('role_id');
$table->string('action');
$table->index('role_id');
$table->index('action');
$table->index(['restrictable_id', 'restrictable_type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('images', function (Blueprint $table) {
$table->dropColumn('uploaded_to');
});
Schema::table('books', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::table('chapters', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::drop('restrictions');
}
}

View File

@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
public function run() public function run()
{ {
$user = factory(BookStack\User::class, 1)->create(); $user = factory(BookStack\User::class, 1)->create();
$role = \BookStack\Role::getDefault(); $role = \BookStack\Role::getRole('editor');
$user->attachRole($role); $user->attachRole($role);

View File

@ -21,6 +21,7 @@
</filter> </filter>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/> <env name="QUEUE_DRIVER" value="sync"/>

View File

@ -118,6 +118,7 @@ module.exports = function (ngApp, events) {
page++; page++;
}); });
} }
$scope.fetchData = fetchData; $scope.fetchData = fetchData;
/** /**
@ -130,12 +131,16 @@ module.exports = function (ngApp, events) {
$http.put(url, this.selectedImage).then((response) => { $http.put(url, this.selectedImage).then((response) => {
events.emit('success', 'Image details updated'); events.emit('success', 'Image details updated');
}, (response) => { }, (response) => {
var errors = response.data; if (response.status === 422) {
var message = ''; var errors = response.data;
Object.keys(errors).forEach((key) => { var message = '';
message += errors[key].join('\n'); Object.keys(errors).forEach((key) => {
}); message += errors[key].join('\n');
events.emit('error', message); });
events.emit('error', message);
} else if (response.status === 403) {
events.emit('error', response.data.error);
}
}); });
}; };
@ -158,6 +163,8 @@ module.exports = function (ngApp, events) {
// Pages failure // Pages failure
if (response.status === 400) { if (response.status === 400) {
$scope.dependantPages = response.data; $scope.dependantPages = response.data;
} else if (response.status === 403) {
events.emit('error', response.data.error);
} }
}); });
}; };
@ -167,7 +174,7 @@ module.exports = function (ngApp, events) {
* @param stringDate * @param stringDate
* @returns {Date} * @returns {Date}
*/ */
$scope.getDate = function(stringDate) { $scope.getDate = function (stringDate) {
return new Date(stringDate); return new Date(stringDate);
}; };

View File

@ -87,6 +87,9 @@ header {
padding-top: $-s; padding-top: $-s;
} }
} }
.dropdown-container {
font-size: 0.9em;
}
} }
form.search-box { form.search-box {

View File

@ -95,13 +95,14 @@
// Sidebar list // Sidebar list
.book-tree { .book-tree {
padding: $-xl 0 0 0; padding: $-l 0 0 0;
position: relative; position: relative;
right: 0; right: 0;
top: 0; top: 0;
transition: ease-in-out 240ms; transition: ease-in-out 240ms;
transition-property: right, border; transition-property: right, border;
border-left: 0px solid #FFF; border-left: 0px solid #FFF;
background-color: #FFF;
&.fixed { &.fixed {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@ -8,4 +8,5 @@ return [
// Pages // Pages
'permission' => 'You do not have permission to access the requested page.', 'permission' => 'You do not have permission to access the requested page.',
'permissionJson' => 'You do not have permission to perform the requested action.'
]; ];

View File

@ -62,7 +62,7 @@
<div class="float right"> <div class="float right">
<div class="links text-center"> <div class="links text-center">
<a href="/books"><i class="zmdi zmdi-book"></i>Books</a> <a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
@if(isset($currentUser) && $currentUser->can('settings-update')) @if(isset($currentUser) && $currentUser->can('settings-manage'))
<a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a> <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
@endif @endif
@if(!isset($signedIn) || !$signedIn) @if(!isset($signedIn) || !$signedIn)

View File

@ -8,7 +8,7 @@
<div class="col-xs-1"></div> <div class="col-xs-1"></div>
<div class="col-xs-11 faded"> <div class="col-xs-11 faded">
<div class="action-buttons"> <div class="action-buttons">
@if($currentUser->can('book-create')) @if($currentUser->can('book-create-all'))
<a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a> <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a>
@endif @endif
</div> </div>
@ -30,7 +30,9 @@
{!! $books->render() !!} {!! $books->render() !!}
@else @else
<p class="text-muted">No books have been created.</p> <p class="text-muted">No books have been created.</p>
<a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> @if(userCan('books-create-all'))
<a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
@endif
@endif @endif
</div> </div>
<div class="col-sm-4 col-sm-offset-1"> <div class="col-sm-4 col-sm-offset-1">

View File

@ -0,0 +1,23 @@
@extends('base')
@section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable>
<h1>Book Restrictions</h1>
@include('form/restriction-form', ['model' => $book])
</div>
@stop

View File

@ -2,23 +2,35 @@
@section('content') @section('content')
<div class="faded-small toolbar" ng-non-bindable> <div class="faded-small toolbar">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="action-buttons faded"> <div class="action-buttons faded">
@if($currentUser->can('page-create')) @if(userCan('page-create', $book))
<a href="{{$book->getUrl() . '/page/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a> <a href="{{$book->getUrl() . '/page/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a>
@endif @endif
@if($currentUser->can('chapter-create')) @if(userCan('chapter-create', $book))
<a href="{{$book->getUrl() . '/chapter/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a> <a href="{{$book->getUrl() . '/chapter/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a>
@endif @endif
@if($currentUser->can('book-update')) @if(userCan('book-update', $book))
<a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
<a href="{{ $book->getUrl() }}/sort" class="text-primary text-button"><i class="zmdi zmdi-sort"></i>Sort</a>
@endif @endif
@if($currentUser->can('book-delete')) @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
<a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> <div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
<ul>
@if(userCan('book-update', $book))
<li><a href="{{ $book->getUrl() }}/sort" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li>
@endif
@if(userCan('restrictions-manage', $book))
<li><a href="{{$book->getUrl()}}/restrict" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Restrict</a></li>
@endif
@if(userCan('book-delete', $book))
<li><a href="{{ $book->getUrl() }}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
@endif
</ul>
</div>
@endif @endif
</div> </div>
</div> </div>
@ -75,6 +87,15 @@
<div class="col-md-4 col-md-offset-1"> <div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div> <div class="margin-top large"></div>
@if($book->restricted)
<p class="text-muted">
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
@endif
</p>
@endif
<div class="search-box"> <div class="search-box">
<form ng-submit="searchBook($event)"> <form ng-submit="searchBook($event)">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book"> <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">

View File

@ -0,0 +1,24 @@
@extends('base')
@section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$chapter->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span>
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable>
<h1>Chapter Restrictions</h1>
@include('form/restriction-form', ['model' => $chapter])
</div>
@stop

View File

@ -12,13 +12,16 @@
</div> </div>
<div class="col-md-8 faded"> <div class="col-md-8 faded">
<div class="action-buttons"> <div class="action-buttons">
@if($currentUser->can('chapter-create')) @if(userCan('page-create', $chapter))
<a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a> <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a>
@endif @endif
@if($currentUser->can('chapter-update')) @if(userCan('chapter-update', $chapter))
<a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
@endif @endif
@if($currentUser->can('chapter-delete')) @if(userCan('restrictions-manage', $chapter))
<a href="{{$chapter->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
@endif
@if(userCan('chapter-delete', $chapter))
<a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> <a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
@endif @endif
</div> </div>
@ -34,10 +37,10 @@
<h1>{{ $chapter->name }}</h1> <h1>{{ $chapter->name }}</h1>
<p class="text-muted">{{ $chapter->description }}</p> <p class="text-muted">{{ $chapter->description }}</p>
@if(count($chapter->pages) > 0) @if(count($pages) > 0)
<div class="page-list"> <div class="page-list">
<hr> <hr>
@foreach($chapter->pages as $page) @foreach($pages as $page)
@include('pages/list-item', ['page' => $page]) @include('pages/list-item', ['page' => $page])
<hr> <hr>
@endforeach @endforeach
@ -60,6 +63,29 @@
</p> </p>
</div> </div>
<div class="col-md-3 col-md-offset-1"> <div class="col-md-3 col-md-offset-1">
<div class="margin-top large"></div>
@if($book->restricted || $chapter->restricted)
<div class="text-muted">
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
@endif
<br>
@endif
@if($chapter->restricted)
@if(userCan('restrictions-manage', $chapter))
<a href="{{ $chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter Restricted
@endif
@endif
</div>
@endif
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<div class="container"> <div class="container">
<h1 class="text-muted">Page Not Found</h1> <h1 class="text-muted">{{ $message or 'Page Not Found' }}</h1>
<p>Sorry, The page you were looking for could not be found.</p> <p>Sorry, The page you were looking for could not be found.</p>
<a href="/" class="button">Return To Home</a> <a href="/" class="button">Return To Home</a>
</div> </div>

View File

@ -0,0 +1,12 @@
<label>
<input value="true" id="{{$name}}" type="checkbox" name="{{$name}}"
@if($errors->has($name)) class="neg" @endif
@if(old($name) || (!old() && isset($model) && $model->$name)) checked="checked" @endif
>
{{ $label }}
</label>
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
@endif

View File

@ -0,0 +1,7 @@
<label>
<input value="true" id="{{$name}}[{{$role->id}}][{{$action}}]" type="checkbox" name="{{$name}}[{{$role->id}}][{{$action}}]"
@if(old($name .'.'.$role->id.'.'.$action) || (!old() && isset($model) && $model->hasRestriction($role->id, $action))) checked="checked" @endif
>
{{ $label }}
</label>

View File

@ -0,0 +1,29 @@
<form action="{{ $model->getUrl() }}/restrict" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
<div class="form-group">
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
</div>
<table class="table">
<tr>
<th>Role</th>
<th @if($model->isA('page')) colspan="3" @else colspan="4" @endif>Actions</th>
</tr>
@foreach($roles as $role)
<tr>
<td>{{ $role->display_name }}</td>
<td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'View', 'action' => 'view'])</td>
@if(!$model->isA('page'))
<td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Create', 'action' => 'create'])</td>
@endif
<td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Update', 'action' => 'update'])</td>
<td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Delete', 'action' => 'delete'])</td>
</tr>
@endforeach
</table>
<a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Restrictions</button>
</form>

View File

@ -0,0 +1,14 @@
@foreach($roles as $role)
<label>
<input value="{{ $role->id }}" id="{{$name}}-{{$role->name}}" type="checkbox" name="{{$name}}[{{$role->name}}]"
@if($errors->has($name)) class="neg" @endif
@if(old($name . '.' . $role->name) || (!old('name') && isset($model) && $model->hasRole($role->name))) checked="checked" @endif
>
{{ $role->display_name }}
</label>
@endforeach
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
@endif

View File

@ -33,10 +33,14 @@
<div class="col-sm-4"> <div class="col-sm-4">
<h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3> <h3><a class="no-color" href="/pages/recently-created">Recently Created Pages</a></h3>
@include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) <div id="recently-created-pages">
@include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact'])
</div>
<h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3> <h3><a class="no-color" href="/pages/recently-updated">Recently Updated Pages</a></h3>
@include('partials/entity-list', ['entities' => $recentlyCreatedPages, 'style' => 'compact']) <div id="recently-updated-pages">
@include('partials/entity-list', ['entities' => $recentlyUpdatedPages, 'style' => 'compact'])
</div>
</div> </div>
<div class="col-sm-4" id="recent-activity"> <div class="col-sm-4" id="recent-activity">

View File

@ -0,0 +1,31 @@
@extends('base')
@section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$page->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
@if($page->hasChapter())
<span class="sep">&raquo;</span>
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
<i class="zmdi zmdi-collection-bookmark"></i>
{{$page->chapter->getShortName()}}
</a>
@endif
<span class="sep">&raquo;</span>
<a href="{{$page->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable>
<h1>Page Restrictions</h1>
@include('form/restriction-form', ['model' => $page])
</div>
@stop

View File

@ -22,17 +22,20 @@
<span dropdown class="dropdown-container"> <span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div> <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
<ul class="wide"> <ul class="wide">
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li> <li><a href="{{$page->getUrl()}}/export/html" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li> <li><a href="{{$page->getUrl()}}/export/pdf" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li> <li><a href="{{$page->getUrl()}}/export/plaintext" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
</ul> </ul>
</span> </span>
@if($currentUser->can('page-update')) @if(userCan('page-update', $page))
<a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a> <a href="{{$page->getUrl()}}/revisions" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
<a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a> <a href="{{$page->getUrl()}}/edit" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
@endif @endif
@if($currentUser->can('page-delete')) @if(userCan('restrictions-manage', $page))
<a href="{{$page->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> <a href="{{$page->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
@endif
@if(userCan('page-delete', $page))
<a href="{{$page->getUrl()}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
@endif @endif
</div> </div>
</div> </div>
@ -67,7 +70,38 @@
</div> </div>
</div> </div>
<div class="col-md-3 print-hidden"> <div class="col-md-3 print-hidden">
<div class="margin-top large"></div>
@if($book->restricted || ($page->chapter && $page->chapter->restricted) || $page->restricted)
<div class="text-muted">
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book restricted
@endif
<br>
@endif
@if($page->chapter && $page->chapter->restricted)
@if(userCan('restrictions-manage', $page->chapter))
<a href="{{ $page->chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter restricted
@endif
<br>
@endif
@if($page->restricted)
@if(userCan('restrictions-manage', $page))
<a href="{{ $page->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Page restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Page restricted
@endif
<br>
@endif
</div>
@endif
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div> </div>

View File

@ -16,8 +16,8 @@
{{ $activity->getText() }} {{ $activity->getText() }}
@if($activity->entity()) @if($activity->entity)
<a href="{{ $activity->entity()->getUrl() }}">{{ $activity->entity()->name }}</a> <a href="{{ $activity->entity->getUrl() }}">{{ $activity->entity->name }}</a>
@endif @endif
@if($activity->extra) "{{$activity->extra}}" @endif @if($activity->extra) "{{$activity->extra}}" @endif

View File

@ -59,7 +59,7 @@
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif> <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
@foreach(\BookStack\Role::all() as $role) @foreach(\BookStack\Role::all() as $role)
<option value="{{$role->id}}" <option value="{{$role->id}}"
@if(\Setting::get('registration-role', \BookStack\Role::getDefault()->id) == $role->id) selected @endif @if(\Setting::get('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
> >
{{ $role->display_name }} {{ $role->display_name }}
</option> </option>

View File

@ -5,6 +5,7 @@
<div class="col-md-12 setting-nav"> <div class="col-md-12 setting-nav">
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a> <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a> <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
<a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,3 @@
<input type="checkbox" name="permissions[{{ $permission }}]"
@if(old('permissions.'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission)))) checked="checked" @endif
value="true">

View File

@ -0,0 +1,15 @@
@extends('base')
@section('content')
@include('settings/navbar', ['selected' => 'roles'])
<div class="container">
<h1>Create New Role</h1>
<form action="/settings/roles/new" method="POST">
@include('settings/roles/form')
</form>
</div>
@stop

View File

@ -0,0 +1,28 @@
@extends('base')
@section('content')
@include('settings/navbar', ['selected' => 'roles'])
<div class="container small" ng-non-bindable>
<h1>Delete Role</h1>
<p>This will delete the role with the name '{{$role->display_name}}'.</p>
<form action="/settings/roles/delete/{{$role->id}}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
@if($role->users->count() > 0)
<div class="form-group">
<p>This role has {{$role->users->count()}} users assigned to it. If you would like to migrate the users from this role select a new role below.</p>
@include('form/role-select', ['options' => $roles, 'name' => 'migration_role_id'])
</div>
@endif
<p class="text-neg">Are you sure you want to delete this role?</p>
<a href="/settings/roles/{{ $role->id }}" class="button">Cancel</a>
<button type="submit" class="button neg">Confirm</button>
</form>
</div>
@stop

View File

@ -0,0 +1,24 @@
@extends('base')
@section('content')
@include('settings/navbar', ['selected' => 'roles'])
<div class="container">
<div class="row">
<div class="col-sm-6">
<h1>Edit Role <small> {{ $role->display_name }}</small></h1>
</div>
<div class="col-sm-6">
<p></p>
<a href="/settings/roles/delete/{{ $role->id }}" class="button neg float right">Delete Role</a>
</div>
</div>
<form action="/settings/roles/{{ $role->id }}" method="POST">
<input type="hidden" name="_method" value="PUT">
@include('settings/roles/form', ['model' => $role])
</form>
</div>
@stop

View File

@ -0,0 +1,117 @@
{!! csrf_field() !!}
<div class="row">
<div class="col-md-6">
<h3>Role Details</h3>
<div class="form-group">
<label for="name">Role Name</label>
@include('form/text', ['name' => 'display_name'])
</div>
<div class="form-group">
<label for="name">Short Role Description</label>
@include('form/text', ['name' => 'description'])
</div>
<h3>System Permissions</h3>
<div class="row">
<div class="col-md-6">
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
</div>
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage user roles</label>
</div>
</div>
<hr class="even">
<div class="row">
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all restrictions</label>
</div>
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage restrictions on own content</label>
</div>
</div>
<hr class="even">
<div class="form-group">
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
</div>
<hr class="even">
</div>
<div class="col-md-6">
<h3>Asset Permissions</h3>
<p>
These permissions control default access to the assets within the system. <br>
Restrictions on Books, Chapters and Pages will override these permissions.
</p>
<table class="table">
<tr>
<th></th>
<th>Create</th>
<th>Edit</th>
<th>Delete</th>
</tr>
<tr>
<td>Books</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All</label>
</td>
</tr>
<tr>
<td>Chapters</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All</label>
</td>
</tr>
<tr>
<td>Pages</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'page-create-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All</label>
</td>
</tr>
<tr>
<td>Images</td>
<td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own</label>
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
</td>
</tr>
</table>
</div>
</div>
<a href="/settings/roles" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Role</button>

View File

@ -0,0 +1,31 @@
@extends('base')
@section('content')
@include('settings/navbar', ['selected' => 'roles'])
<div class="container small">
<h1>User Roles</h1>
<p>
<a href="/settings/roles/new" class="text-pos"><i class="zmdi zmdi-lock-open"></i>Add new role</a>
</p>
<table class="table">
<tr>
<th>Role Name</th>
<th></th>
<th class="text-right">Users</th>
</tr>
@foreach($roles as $role)
<tr>
<td><a href="/settings/roles/{{ $role->id }}">{{ $role->display_name }}</a></td>
<td>{{ $role->description }}</td>
<td class="text-right">{{ $role->users->count() }}</td>
</tr>
@endforeach
</table>
</div>
@stop

View File

@ -3,21 +3,21 @@
@include('form.text', ['name' => 'name']) @include('form.text', ['name' => 'name'])
</div> </div>
@if($currentUser->can('user-update')) @if(userCan('users-manage'))
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">Email</label>
@include('form.text', ['name' => 'email']) @include('form.text', ['name' => 'email'])
</div> </div>
@endif @endif
@if($currentUser->can('user-update')) @if(userCan('users-manage'))
<div class="form-group"> <div class="form-group">
<label for="role">User Role</label> <label for="role">User Role</label>
@include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name']) @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()])
</div> </div>
@endif @endif
@if($currentUser->can('user-update')) @if(userCan('users-manage'))
<div class="form-group"> <div class="form-group">
<label for="external_auth_id">External Authentication ID</label> <label for="external_auth_id">External Authentication ID</label>
@include('form.text', ['name' => 'external_auth_id']) @include('form.text', ['name' => 'external_auth_id'])

View File

@ -8,10 +8,10 @@
@include('form.text', ['name' => 'email']) @include('form.text', ['name' => 'email'])
</div> </div>
@if($currentUser->can('user-update')) @if(userCan('users-manage'))
<div class="form-group"> <div class="form-group">
<label for="role">User Role</label> <label for="role">User Role</label>
@include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name']) @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()])
</div> </div>
@endif @endif

View File

@ -8,7 +8,7 @@
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Users</h1> <h1>Users</h1>
@if($currentUser->can('user-create')) @if(userCan('users-manage'))
<p> <p>
<a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a> <a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a>
</p> </p>
@ -18,30 +18,32 @@
<th></th> <th></th>
<th>Name</th> <th>Name</th>
<th>Email</th> <th>Email</th>
<th>User Type</th> <th>User Roles</th>
</tr> </tr>
@foreach($users as $user) @foreach($users as $user)
<tr> <tr>
<td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td> <td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
<td> <td>
@if($currentUser->can('user-update') || $currentUser->id == $user->id) @if(userCan('users-manage') || $currentUser->id == $user->id)
<a href="/settings/users/{{$user->id}}"> <a href="/settings/users/{{$user->id}}">
@endif @endif
{{ $user->name }} {{ $user->name }}
@if($currentUser->can('user-update') || $currentUser->id == $user->id) @if(userCan('users-manage') || $currentUser->id == $user->id)
</a> </a>
@endif @endif
</td> </td>
<td> <td>
@if($currentUser->can('user-update') || $currentUser->id == $user->id) @if(userCan('users-manage') || $currentUser->id == $user->id)
<a href="/settings/users/{{$user->id}}"> <a href="/settings/users/{{$user->id}}">
@endif @endif
{{ $user->email }} {{ $user->email }}
@if($currentUser->can('user-update') || $currentUser->id == $user->id) @if(userCan('users-manage') || $currentUser->id == $user->id)
</a> </a>
@endif @endif
</td> </td>
<td>{{ $user->role->display_name }}</td> <td>
<small> {{ $user->roles->implode('display_name', ', ') }}</small>
</td>
</tr> </tr>
@endforeach @endforeach
</table> </table>

View File

@ -133,12 +133,12 @@ class AuthTest extends TestCase
->click('Add new user') ->click('Add new user')
->type($user->name, '#name') ->type($user->name, '#name')
->type($user->email, '#email') ->type($user->email, '#email')
->select(2, '#role') ->check('roles[admin]')
->type($user->password, '#password') ->type($user->password, '#password')
->type($user->password, '#password-confirm') ->type($user->password, '#password-confirm')
->press('Save') ->press('Save')
->seeInDatabase('users', $user->toArray())
->seePageIs('/settings/users') ->seePageIs('/settings/users')
->seeInDatabase('users', $user->toArray())
->see($user->name); ->see($user->name);
} }

View File

@ -225,4 +225,22 @@ class EntityTest extends TestCase
->seePageIs($newPageUrl); ->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);
}
} }

407
tests/RestrictionsTest.php Normal file
View File

@ -0,0 +1,407 @@
<?php
class RestrictionsTest extends TestCase
{
protected $user;
public function setUp()
{
parent::setUp();
$this->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);
}
}

510
tests/RolesTest.php Normal file
View File

@ -0,0 +1,510 @@
<?php
class RolesTest extends TestCase
{
protected $user;
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($permissions = [])
{
$permissionRepo = app('BookStack\Repos\PermissionsRepo');
$roleData = factory(\BookStack\Role::class)->make()->toArray();
$roleData['permissions'] = array_flip($permissions);
return $permissionRepo->saveNewRole($roleData);
}
public function test_admin_can_see_settings()
{
$this->asAdmin()->visit('/settings')->see('Settings');
}
public function test_cannot_delete_admin_role()
{
$adminRole = \BookStack\Role::getRole('admin');
$deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
$this->asAdmin()->visit($deletePageUrl)
->press('Confirm')
->seePageIs($deletePageUrl)
->see('cannot be deleted');
}
public function test_role_cannot_be_deleted_if_default()
{
$newRole = $this->createNewRole();
$this->setSettings(['registration-role' => $newRole->id]);
$deletePageUrl = '/settings/roles/delete/' . $newRole->id;
$this->asAdmin()->visit($deletePageUrl)
->press('Confirm')
->seePageIs($deletePageUrl)
->see('cannot be deleted');
}
public function test_role_create_update_delete_flow()
{
$testRoleName = 'Test Role';
$testRoleDesc = 'a little test description';
$testRoleUpdateName = 'An Super Updated role';
// Creation
$this->asAdmin()->visit('/settings')
->click('Roles')
->seePageIs('/settings/roles')
->click('Add new role')
->type('Test Role', 'display_name')
->type('A little test description', 'description')
->press('Save Role')
->seeInDatabase('roles', ['display_name' => $testRoleName, 'name' => 'test-role', 'description' => $testRoleDesc])
->seePageIs('/settings/roles');
// Updating
$this->asAdmin()->visit('/settings/roles')
->see($testRoleDesc)
->click($testRoleName)
->type($testRoleUpdateName, '#display_name')
->press('Save Role')
->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'name' => 'test-role', 'description' => $testRoleDesc])
->seePageIs('/settings/roles');
// Deleting
$this->asAdmin()->visit('/settings/roles')
->click($testRoleUpdateName)
->click('Delete Role')
->see($testRoleUpdateName)
->press('Confirm')
->seePageIs('/settings/roles')
->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);
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\DomCrawler\Crawler;
class TestCase extends Illuminate\Foundation\Testing\TestCase class TestCase extends Illuminate\Foundation\Testing\TestCase
{ {
@ -32,7 +33,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
public function asAdmin() public function asAdmin()
{ {
if($this->admin === null) { if($this->admin === null) {
$this->admin = \BookStack\User::find(1); $adminRole = \BookStack\Role::getRole('admin');
$this->admin = $adminRole->users->first();
} }
return $this->actingAs($this->admin); return $this->actingAs($this->admin);
} }
@ -78,8 +80,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
protected function getNewUser($attributes = []) protected function getNewUser($attributes = [])
{ {
$user = factory(\BookStack\User::class)->create($attributes); $user = factory(\BookStack\User::class)->create($attributes);
$userRepo = app('BookStack\Repos\UserRepo'); $role = \BookStack\Role::getRole('editor');
$userRepo->attachDefaultRole($user); $user->attachRole($role);;
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; return $user;
} }
@ -110,6 +123,40 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $this; return $this;
} }
/**
* Assert that the current page matches a given URI.
*
* @param string $uri
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->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. * Click the text within the selected element.
* @param $parentElement * @param $parentElement