Including testing to check permissions applied to listed references.
namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionApplicator;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceController extends Controller
{
/** @var Page $page */
$page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
+ $references = $this->getEntityReferences($page);
- $baseQuery = $page->referencesTo()
+ return view('pages.references', [
+ 'page' => $page,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Display the references to a given chapter.
+ */
+ public function chapter(string $bookSlug, string $chapterSlug)
+ {
+ /** @var Chapter $chapter */
+ $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
+ $references = $this->getEntityReferences($chapter);
+
+ return view('chapters.references', [
+ 'chapter' => $chapter,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Display the references to a given book.
+ */
+ public function book(string $slug)
+ {
+ $book = Book::visible()->where('slug', '=', $slug)->firstOrFail();
+ $references = $this->getEntityReferences($book);
+
+ return view('books.references', [
+ 'book' => $book,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Display the references to a given shelf.
+ */
+ public function shelf(string $slug)
+ {
+ $shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail();
+ $references = $this->getEntityReferences($shelf);
+
+ return view('shelves.references', [
+ 'shelf' => $shelf,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Query the references for the given entities.
+ * Loads the commonly required relations while taking permissions into account.
+ */
+ protected function getEntityReferences(Entity $entity): Collection
+ {
+ $baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass())
->with([
'from' => fn(Relation $query) => $query->select(Page::$listAttributes),
'from_type'
)->get();
- return view('pages.references', [
- 'page' => $page,
- 'references' => $references,
- ]);
+ return $references;
}
}
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+
+ <div class="container small">
+
+ <div class="my-s">
+ @include('entities.breadcrumbs', ['crumbs' => [
+ $book,
+ $book->getUrl('/references') => [
+ 'text' => trans('entities.references'),
+ 'icon' => 'reference',
+ ]
+ ]])
+ </div>
+
+ @include('entities.references', ['references' => $references])
+ </div>
+
+@stop
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+
+ <div class="container small">
+
+ <div class="my-s">
+ @include('entities.breadcrumbs', ['crumbs' => [
+ $chapter->book,
+ $chapter,
+ $chapter->getUrl('/references') => [
+ 'text' => trans('entities.references'),
+ 'icon' => 'reference',
+ ]
+ ]])
+ </div>
+
+ @include('entities.references', ['references' => $references])
+ </div>
+
+@stop
--- /dev/null
+<main class="card content-wrap">
+ <h1 class="list-heading">{{ trans('entities.references') }}</h1>
+ <p>{{ trans('entities.references_to_desc') }}</p>
+
+ @if(count($references) > 0)
+ <div class="book-contents">
+ @include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])
+ </div>
+ @else
+ <p class="text-muted italic">{{ trans('entities.references_none') }}</p>
+ @endif
+
+</main>
\ No newline at end of file
]])
</div>
- <main class="card content-wrap">
- <h1 class="list-heading">{{ trans('entities.references') }}</h1>
- <p>{{ trans('entities.references_to_desc') }}</p>
-
- @if(count($references) > 0)
- <div class="book-contents">
- @include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])
- </div>
- @else
- <p class="text-muted italic">{{ trans('entities.references_none') }}</p>
- @endif
-
- </main>
+ @include('entities.references', ['references' => $references])
</div>
@stop
--- /dev/null
+@extends('layouts.simple')
+
+@section('body')
+
+ <div class="container small">
+
+ <div class="my-s">
+ @include('entities.breadcrumbs', ['crumbs' => [
+ $shelf,
+ $shelf->getUrl('/references') => [
+ 'text' => trans('entities.references'),
+ 'icon' => 'reference',
+ ]
+ ]])
+ </div>
+
+ @include('entities.references', ['references' => $references])
+ </div>
+
+@stop
Route::get('/shelves/{slug}/permissions', [BookshelfController::class, 'showPermissions']);
Route::put('/shelves/{slug}/permissions', [BookshelfController::class, 'permissions']);
Route::post('/shelves/{slug}/copy-permissions', [BookshelfController::class, 'copyPermissions']);
+ Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
// Book Creation
Route::get('/shelves/{shelfSlug}/create-book', [BookController::class, 'create']);
Route::post('/books/{bookSlug}/convert-to-shelf', [BookController::class, 'convertToShelf']);
Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
+ Route::get('/books/{slug}/references', [ReferenceController::class, 'book']);
Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
Route::get('/books/{bookSlug}/export/pdf', [BookExportController::class, 'pdf']);
Route::get('/books/{bookSlug}/export/markdown', [BookExportController::class, 'markdown']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ChapterExportController::class, 'markdown']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ChapterExportController::class, 'plainText']);
Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'permissions']);
+ Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [ChapterController::class, 'showDelete']);
Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'destroy']);
$this->assertDatabaseMissing('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]);
}
+ public function test_references_to_visible_on_references_page()
+ {
+ $entities = $this->getEachEntityType();
+ $this->asEditor();
+ foreach ($entities as $entity) {
+ $this->createReference($entities['page'], $entity);
+ }
+
+ foreach ($entities as $entity) {
+ $resp = $this->get($entity->getUrl('/references'));
+ $resp->assertSee('References');
+ $resp->assertSee($entities['page']->name);
+ $resp->assertDontSee('There are no tracked references');
+ }
+ }
+
+ public function test_reference_not_visible_if_view_permission_does_not_permit()
+ {
+ /** @var Page $page */
+ /** @var Page $pageB */
+ $page = Page::query()->first();
+ $pageB = Page::query()->where('id', '!=', $page->id)->first();
+ $this->createReference($pageB, $page);
+
+ $this->setEntityRestrictions($pageB);
+
+ $this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name);
+ $this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name);
+ }
+
+ public function test_reference_page_shows_empty_state_with_no_references()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+
+ $this->asEditor()
+ ->get($page->getUrl('/references'))
+ ->assertSee('There are no tracked references');
+ }
+
protected function createReference(Model $from, Model $to)
{
(new Reference())->forceFill([
}
/**
- * @return Entity[]
+ * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf}
*/
protected function getEachEntityType(): array
{