Also updated many styles within areas affected by the new permission and roles system.
// Which will include the basic message to point the user roughly to the cause.
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
- return response()->view('errors/500', ['message' => $message], 500);
+ $code = ($e->getCode() === 0) ? 500 : $e->getCode();
+ return response()->view('errors/' . $code, ['message' => $message], $code);
}
return parent::render($request, $e);
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+
+class NotFoundException extends PrettyException {
+
+ /**
+ * NotFoundException constructor.
+ * @param string $message
+ */
+ public function __construct($message = 'Item not found')
+ {
+ parent::__construct($message, 404);
+ }
+}
\ No newline at end of file
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
- return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
+ $pages = $this->chapterRepo->getChildren($chapter);
+ return view('chapters/show', [
+ 'book' => $book,
+ 'chapter' => $chapter,
+ 'current' => $chapter,
+ 'sidebarTree' => $sidebarTree,
+ 'pages' => $pages
+ ]);
}
/**
<?php namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
- } catch (NotFoundHttpException $e) {
+ } catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
return redirect($page->getUrl());
<?php namespace BookStack\Repos;
use Activity;
+use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService;
use Illuminate\Support\Str;
use BookStack\Book;
* Get a book by slug
* @param $slug
* @return mixed
+ * @throws NotFoundException
*/
public function getBySlug($slug)
{
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
- if ($book === null) abort(404);
+ if ($book === null) throw new NotFoundException('Book not found');
return $book;
}
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
+ $book->restrictions()->delete();
$book->delete();
}
public function getChildren(Book $book)
{
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
- $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
+ $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
$pages = $pageQuery->get();
- $chapterQuery = $book->chapters()->with('pages');
- $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
+ $chapterQuery = $book->chapters()->with(['pages' => function($query) {
+ $this->restrictionService->enforcePageRestrictions($query, 'view');
+ }]);
+ $chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
$children = $pages->merge($chapters);
$bookSlug = $book->slug;
use Activity;
+use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService;
use Illuminate\Support\Str;
use BookStack\Chapter;
* @param $slug
* @param $bookId
* @return mixed
+ * @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
- if ($chapter === null) abort(404);
+ if ($chapter === null) throw new NotFoundException('Chapter not found');
return $chapter;
}
+ /**
+ * Get the child items for a chapter
+ * @param Chapter $chapter
+ */
+ public function getChildren(Chapter $chapter)
+ {
+ return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
+ }
+
/**
* Create a new chapter from request input.
* @param $input
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
+ $chapter->restrictions()->delete();
$chapter->delete();
}
use Activity;
use BookStack\Book;
use BookStack\Chapter;
+use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
* @param $slug
* @param $bookId
* @return mixed
+ * @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
- if ($page === null) throw new NotFoundHttpException('Page not found');
+ if ($page === null) throw new NotFoundException('Page not found');
return $page;
}
Activity::removeEntity($page);
$page->views()->delete();
$page->revisions()->delete();
+ $page->restrictions()->delete();
$page->delete();
}
public function __construct()
{
$user = auth()->user();
- $this->userRoles = $user ? auth()->user()->roles->pluck('id') : false;
+ $this->userRoles = $user ? auth()->user()->roles->pluck('id') : [];
$this->isAdmin = $user ? auth()->user()->hasRole('admin') : false;
}
+ /**
+ * Checks if an entity has a restriction set upon it.
+ * @param Entity $entity
+ * @param $action
+ * @return bool
+ */
public function checkIfEntityRestricted(Entity $entity, $action)
{
if ($this->isAdmin) return true;
});
});
})
+ // 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');
});
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
->where('restricted', '=', false)
- ->orWhereExists(function ($query) {
- $this->checkRestrictionsQuery($query, 'books', 'Book');
+ ->orWhere(function ($query) {
+ $query->where('restricted', '=', true)->whereExists(function ($query) {
+ $this->checkRestrictionsQuery($query, 'books', 'Book');
+ });
});
});
}
<?php
-if (! function_exists('versioned_asset')) {
+if (!function_exists('versioned_asset')) {
/**
* Get the path to a versioned file.
*
- * @param string $file
+ * @param string $file
* @return string
*
* @throws \InvalidArgumentException
*/
function userCan($permission, \BookStack\Ownable $ownable = null)
{
+ if (!auth()->check()) return false;
if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission);
}
$permissionBaseName = strtolower($permission) . '-';
$hasPermission = false;
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
- if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
+ if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
- if(!$ownable instanceof \BookStack\Entity) return $hasPermission;
+ if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
// Check restrictions on the entitiy
$restrictionService = app('BookStack\Services\RestrictionService');
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
- $role = \BookStack\Role::getDefault();
+ $role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
</filter>
<php>
<env name="APP_ENV" value="testing"/>
+ <env name="APP_DEBUG" value="false"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
padding-top: $-s;
}
}
+ .dropdown-container {
+ font-size: 0.9em;
+ }
}
form.search-box {
// Sidebar list
.book-tree {
- padding: $-xl 0 0 0;
+ padding: $-l 0 0 0;
position: relative;
right: 0;
top: 0;
transition: ease-in-out 240ms;
transition-property: right, border;
border-left: 0px solid #FFF;
+ background-color: #FFF;
&.fixed {
position: fixed;
top: 0;
{!! $books->render() !!}
@else
<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
</div>
<div class="col-sm-4 col-sm-offset-1">
@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])
@section('content')
- <div class="faded-small toolbar" ng-non-bindable>
+ <div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-12">
@endif
@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->getUrl() }}/sort" class="text-primary text-button"><i class="zmdi zmdi-sort"></i>Sort</a>
@endif
- @if(userCan('restrictions-manage', $book))
- <a href="{{$book->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
- @endif
- @if(userCan('book-delete', $book))
- <a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
+ @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
+ <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
</div>
</div>
<div class="col-md-4 col-md-offset-1">
<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">
<form ng-submit="searchBook($event)">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">
@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">»</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])
<h1>{{ $chapter->name }}</h1>
<p class="text-muted">{{ $chapter->description }}</p>
- @if(count($chapter->pages) > 0)
+ @if(count($pages) > 0)
<div class="page-list">
<hr>
- @foreach($chapter->pages as $page)
+ @foreach($pages as $page)
@include('pages/list-item', ['page' => $page])
<hr>
@endforeach
</p>
</div>
<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])
</div>
</div>
<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>
<a href="/" class="button">Return To Home</a>
</div>
<input type="hidden" name="_method" value="PUT">
<div class="form-group">
- @include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this page?'])
+ @include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
</div>
<table class="table">
@endforeach
</table>
+ <a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Restrictions</button>
</form>
\ No newline at end of file
@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">»</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">»</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>
</div>
<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])
</div>
<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'])
<label for="name">Short Role Description</label>
@include('form/text', ['name' => 'description'])
</div>
- <hr class="even">
+ <h3>System Permissions</h3>
<div class="row">
<div class="col-md-6">
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
<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>
</div>
</div>
+
+<a href="/settings/roles" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Role</button>
\ No newline at end of file
@include('settings/navbar', ['selected' => 'roles'])
- <div class="container">
+ <div class="container small">
<h1>User Roles</h1>
--- /dev/null
+<?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);
+ }
+
+}
<?php
use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Symfony\Component\DomCrawler\Crawler;
class TestCase extends Illuminate\Foundation\Testing\TestCase
{
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.
* @param $parentElement