Compare commits

...

6 Commits

Author SHA1 Message Date
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
2acef3c2ec Fixed issue where restricted page content in plaintext export
The content of pages made non-viewable to a user via permissions, within a visible parent, could be seen via the plaintext export option. Before v0.30.6 this would have applied only to scenarios where all pages within the chapter were made non-visible. In v0.30.6 this would make all pages within the chapter visible.

As per #2414
2020-12-18 13:56:00 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
3f3fad7113 Fixed book-tree-gen page visibility issue
When book trees were generated, pages in chapters where ALL pages within
were not supposed to be visibile, would be visible due to the code
falling back on the raw relation which would not account for
permissions.

This has now been changed so that a custom 'visible_pages' attribute is set and used by any book tree structures, to ensure it does not fall back to the raw relation.

Added an extra test to cover.

For #2414
2020-12-17 17:31:18 +00:00
13 changed files with 106 additions and 51 deletions

View File

@@ -5,7 +5,6 @@ use Illuminate\Support\Collection;
/**
* Class Chapter
* @property Collection<Page> $pages
* @package BookStack\Entities
*/
class Chapter extends BookChild
{
@@ -52,15 +51,6 @@ class Chapter extends BookChild
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**
* Check if this chapter has any child pages.
* @return bool
*/
public function hasChildren()
{
return count($this->pages) > 0;
}
/**
* Get the visible pages in this chapter.
*/

View File

@@ -203,7 +203,7 @@ class ExportService
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) {
foreach ($chapter->getVisiblePages() as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
@@ -214,7 +214,7 @@ class ExportService
*/
public function bookToPlainText(Book $book): string
{
$bookTree = (new BookContents($book))->getTree(false, true);
$bookTree = (new BookContents($book))->getTree(false, false);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {

View File

@@ -53,12 +53,16 @@ class BookContents
$pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
$chapter = $chapterMap->get($chapter_id);
if ($chapter) {
$chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
$chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
} else {
$lonePages = $lonePages->concat($pages);
}
});
$chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
$chapter->setAttribute('visible_pages', collect([]));
});
$all->each(function (Entity $entity) use ($renderPages) {
$entity->setRelation('book', $this->book);

View File

@@ -112,7 +112,7 @@ class ImageRepo
if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') {
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
$validPageIds = $contextPage->book->pages()->visible()->get(['id'])->pluck('id')->toArray();
$query->whereIn('uploaded_to', $validPageIds);
}
};

View File

@@ -41,9 +41,9 @@
<ul class="contents">
@foreach($bookChildren as $bookChild)
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
@if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
<ul>
@foreach($bookChild->pages as $page)
@foreach($bookChild->visible_pages as $page)
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
@endforeach
</ul>
@@ -59,8 +59,8 @@
@if($bookChild->isA('chapter'))
<p>{{ $bookChild->description }}</p>
@if(count($bookChild->pages) > 0)
@foreach($bookChild->pages as $page)
@if(count($bookChild->visible_pages) > 0)
@foreach($bookChild->visible_pages as $page)
<div class="page-break"></div>
<div class="chapter-hint">{{$bookChild->name}}</div>
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>

View File

@@ -28,7 +28,7 @@
</div>
@if($bookChild->isA('chapter'))
<ul>
@foreach($bookChild->pages as $page)
@foreach($bookChild->visible_pages as $page)
<li class="text-page"
data-id="{{$page->id}}" data-type="page"
data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"

View File

@@ -1,10 +1,10 @@
<div class="chapter-child-menu">
<button chapter-toggle type="button" aria-expanded="{{ $isOpen ? 'true' : 'false' }}"
class="text-muted @if($isOpen) open @endif">
@icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
@icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->visible_pages->count()) }}</span>
</button>
<ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
@foreach($bookChild->pages as $childPage)
@foreach($bookChild->visible_pages as $childPage)
<li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}" role="presentation">
@include('partials.entity-list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
</li>

View File

@@ -1,4 +1,6 @@
<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->hasChildren()) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
{{--This view display child pages in a list if pre-loaded onto a 'visible_pages' property,--}}
{{--To ensure that the pages have been loaded efficiently with permissions taken into account.--}}
<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->visible_pages->count() > 0) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
<span class="icon text-chapter">@icon('chapter')</span>
<div class="content">
<h4 class="entity-list-item-name break-text">{{ $chapter->name }}</h4>
@@ -7,16 +9,16 @@
</div>
</div>
</a>
@if ($chapter->hasChildren())
@if ($chapter->visible_pages->count() > 0)
<div class="chapter chapter-expansion">
<span class="icon text-chapter">@icon('page')</span>
<div class="content">
<button type="button" chapter-toggle
aria-expanded="false"
class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></button>
class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->visible_pages->count()) }}</span></button>
<div class="inset-list">
<div class="entity-list-item-children">
@include('partials.entity-list', ['entities' => $chapter->pages])
@include('partials.entity-list', ['entities' => $chapter->visible_pages])
</div>
</div>
</div>

View File

@@ -15,7 +15,7 @@
<li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
@include('partials.entity-list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
@if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
<div class="entity-list-item no-hover">
<span role="presentation" class="icon text-chapter"></span>
<div class="content">

View File

@@ -1,24 +0,0 @@
<div class="page-list">
@if(count($pages) > 0)
@foreach($pages as $pageIndex => $page)
<div class="anim searchResult" style="animation-delay: {{$pageIndex*50 . 'ms'}};">
@include('pages.list-item', ['page' => $page])
<hr>
</div>
@endforeach
@else
<p class="text-muted">{{ trans('entities.search_no_pages') }}</p>
@endif
</div>
@if(count($chapters) > 0)
<div class="page-list">
@foreach($chapters as $chapterIndex => $chapter)
<div class="anim searchResult" style="animation-delay: {{($chapterIndex+count($pages))*50 . 'ms'}};">
@include('chapters.list-item', ['chapter' => $chapter, 'hidePages' => true])
<hr>
</div>
@endforeach
</div>
@endif

View File

@@ -0,0 +1,67 @@
<?php namespace Tests\Permissions;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use Illuminate\Support\Str;
use Tests\TestCase;
class ExportPermissionsTest extends TestCase
{
public function test_page_content_without_view_access_hidden_on_chapter_export()
{
$chapter = Chapter::query()->first();
$page = $chapter->pages()->firstOrFail();
$pageContent = Str::random(48);
$page->html = '<p>' . $pageContent . '</p>';
$page->save();
$viewer = $this->getViewer();
$this->actingAs($viewer);
$formats = ['html', 'plaintext'];
foreach ($formats as $format) {
$resp = $this->get($chapter->getUrl("export/{$format}"));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertSee($pageContent);
}
$this->setEntityRestrictions($page, []);
foreach ($formats as $format) {
$resp = $this->get($chapter->getUrl("export/{$format}"));
$resp->assertStatus(200);
$resp->assertDontSee($page->name);
$resp->assertDontSee($pageContent);
}
}
public function test_page_content_without_view_access_hidden_on_book_export()
{
$book = Book::query()->first();
$page = $book->pages()->firstOrFail();
$pageContent = Str::random(48);
$page->html = '<p>' . $pageContent . '</p>';
$page->save();
$viewer = $this->getViewer();
$this->actingAs($viewer);
$formats = ['html', 'plaintext'];
foreach ($formats as $format) {
$resp = $this->get($book->getUrl("export/{$format}"));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertSee($pageContent);
}
$this->setEntityRestrictions($page, []);
foreach ($formats as $format) {
$resp = $this->get($book->getUrl("export/{$format}"));
$resp->assertStatus(200);
$resp->assertDontSee($page->name);
$resp->assertDontSee($pageContent);
}
}
}

View File

@@ -490,6 +490,22 @@ class RestrictionsTest extends BrowserKitTest
->dontSee($page->name);
}
public function test_restricted_chapter_pages_not_visible_on_book_page()
{
$chapter = Chapter::query()->first();
$this->actingAs($this->user)
->visit($chapter->book->getUrl())
->see($chapter->pages->first()->name);
foreach ($chapter->pages as $page) {
$this->setEntityRestrictions($page, []);
}
$this->actingAs($this->user)
->visit($chapter->book->getUrl())
->dontSee($chapter->pages->first()->name);
}
public function test_bookshelf_update_restriction_override()
{
$shelf = Bookshelf::first();

View File

@@ -1 +1 @@
v0.30.5
v0.30.7