1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-08-07 23:03:00 +03:00

Merge branch 'development' into default-templates

This commit is contained in:
Dan Brown
2023-12-11 11:41:43 +00:00
1548 changed files with 46899 additions and 21914 deletions

View File

@@ -0,0 +1,128 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Api\ApiEntityListFormatter;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Http\ApiController;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BookApiController extends ApiController
{
protected BookRepo $bookRepo;
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
}
/**
* Get a listing of books visible to the user.
*/
public function list()
{
$books = Book::visible();
return $this->apiListingResponse($books, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
]);
}
/**
* Create a new book in the system.
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
* If the 'image' property is null then the book cover image will be removed.
*
* @throws ValidationException
*/
public function create(Request $request)
{
$this->checkPermission('book-create-all');
$requestData = $this->validate($request, $this->rules()['create']);
$book = $this->bookRepo->create($requestData);
return response()->json($book);
}
/**
* View the details of a single book.
* The response data will contain 'content' property listing the chapter and pages directly within, in
* the same structure as you'd see within the BookStack interface when viewing a book. Top-level
* contents will have a 'type' property to distinguish between pages & chapters.
*/
public function read(string $id)
{
$book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($id);
$contents = (new BookContents($book))->getTree(true, false)->all();
$contentsApiData = (new ApiEntityListFormatter($contents))
->withType()
->withField('pages', function (Entity $entity) {
if ($entity instanceof Chapter) {
return (new ApiEntityListFormatter($entity->pages->all()))->format();
}
return null;
})->format();
$book->setAttribute('contents', $contentsApiData);
return response()->json($book);
}
/**
* Update the details of a single book.
* The cover image of a book can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
* If the 'image' property is null then the book cover image will be removed.
*
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$book = Book::visible()->findOrFail($id);
$this->checkOwnablePermission('book-update', $book);
$requestData = $this->validate($request, $this->rules()['update']);
$book = $this->bookRepo->update($book, $requestData);
return response()->json($book);
}
/**
* Delete a single book.
* This will typically send the book to the recycle bin.
*
* @throws \Exception
*/
public function delete(string $id)
{
$book = Book::visible()->findOrFail($id);
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
return response('', 204);
}
protected function rules(): array
{
return [
'create' => [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'tags' => ['array'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
],
'update' => [
'name' => ['string', 'min:1', 'max:255'],
'description' => ['string', 'max:1000'],
'tags' => ['array'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
],
];
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityQueries;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
use BookStack\Entities\Tools\HierarchyTransformer;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
class BookController extends Controller
{
protected BookRepo $bookRepo;
protected ShelfContext $shelfContext;
protected ReferenceFetcher $referenceFetcher;
public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo, ReferenceFetcher $referenceFetcher)
{
$this->bookRepo = $bookRepo;
$this->shelfContext = $entityContextManager;
$this->referenceFetcher = $referenceFetcher;
}
/**
* Display a listing of the book.
*/
public function index(Request $request)
{
$view = setting()->getForCurrentUser('books_view_type');
$listOptions = SimpleListOptions::fromRequest($request, 'books')->withSortOptions([
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
]);
$books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
$recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
$popular = $this->bookRepo->getPopular(4);
$new = $this->bookRepo->getRecentlyCreated(4);
$this->shelfContext->clearShelfContext();
$this->setPageTitle(trans('entities.books'));
return view('books.index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'view' => $view,
'listOptions' => $listOptions,
]);
}
/**
* Show the form for creating a new book.
*/
public function create(string $shelfSlug = null)
{
$this->checkPermission('book-create-all');
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$this->setPageTitle(trans('entities.books_create'));
$templates = Page::visible()
->where('template', '=', true)
->orderBy('name', 'asc')
->get();
return view('books.create', [
'bookshelf' => $bookshelf,
'templates' => $templates,
]);
}
/**
* Store a newly created book in storage.
*
* @throws ImageUploadException
* @throws ValidationException
*/
public function store(Request $request, string $shelfSlug = null)
{
$this->checkPermission('book-create-all');
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
'tags' => ['array'],
'default_template' => ['nullable', 'exists:pages,id'],
]);
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$book = $this->bookRepo->create($validated);
if ($bookshelf) {
$bookshelf->appendBook($book);
Activity::add(ActivityType::BOOKSHELF_UPDATE, $bookshelf);
}
return redirect($book->getUrl());
}
/**
* Display the specified book.
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
$book = $this->bookRepo->getBySlug($slug);
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->scopes('visible')->get();
View::incrementFor($book);
if ($request->has('shelf')) {
$this->shelfContext->setShelfContext(intval($request->get('shelf')));
}
$this->setPageTitle($book->getShortName());
return view('books.show', [
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves,
'watchOptions' => new UserEntityWatchOptions(user(), $book),
'activity' => $activities->entityActivity($book, 20, 1),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($book),
]);
}
/**
* Show the form for editing the specified book.
*/
public function edit(string $slug)
{
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
$templates = Page::visible()
->where('template', '=', true)
->orderBy('name', 'asc')
->get();
return view('books.edit', ['book' => $book, 'current' => $book, 'templates' => $templates]);
}
/**
* Update the specified book in storage.
*
* @throws ImageUploadException
* @throws ValidationException
* @throws Throwable
*/
public function update(Request $request, string $slug)
{
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
'tags' => ['array'],
'default_template' => ['nullable', 'exists:pages,id'],
]);
if ($request->has('image_reset')) {
$validated['image'] = null;
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
unset($validated['image']);
}
$book = $this->bookRepo->update($book, $validated);
return redirect($book->getUrl());
}
/**
* Shows the page to confirm deletion.
*/
public function showDelete(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
* Remove the specified book from the system.
*
* @throws Throwable
*/
public function destroy(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
return redirect('/books');
}
/**
* Show the view to copy a book.
*
* @throws NotFoundException
*/
public function showCopy(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-view', $book);
session()->flashInput(['name' => $book->name]);
return view('books.copy', [
'book' => $book,
]);
}
/**
* Create a copy of a book within the requested target destination.
*
* @throws NotFoundException
*/
public function copy(Request $request, Cloner $cloner, string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-view', $book);
$this->checkPermission('book-create-all');
$newName = $request->get('name') ?: $book->name;
$bookCopy = $cloner->cloneBook($book, $newName);
$this->showSuccessNotification(trans('entities.books_copy_success'));
return redirect($bookCopy->getUrl());
}
/**
* Convert the chapter to a book.
*/
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission('book-delete', $book);
$this->checkPermission('bookshelf-create-all');
$this->checkPermission('book-create-all');
$shelf = $transformer->transformBookToShelf($book);
return redirect($shelf->getUrl());
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;
class BookExportApiController extends ApiController
{
protected $exportFormatter;
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}
/**
* Export a book as a PDF file.
*
* @throws Throwable
*/
public function exportPdf(int $id)
{
$book = Book::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->download()->directly($pdfContent, $book->slug . '.pdf');
}
/**
* Export a book as a contained HTML file.
*
* @throws Throwable
*/
public function exportHtml(int $id)
{
$book = Book::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->download()->directly($htmlContent, $book->slug . '.html');
}
/**
* Export a book as a plain text file.
*/
public function exportPlainText(int $id)
{
$book = Book::visible()->findOrFail($id);
$textContent = $this->exportFormatter->bookToPlainText($book);
return $this->download()->directly($textContent, $book->slug . '.txt');
}
/**
* Export a book as a markdown file.
*/
public function exportMarkdown(int $id)
{
$book = Book::visible()->findOrFail($id);
$markdown = $this->exportFormatter->bookToMarkdown($book);
return $this->download()->directly($markdown, $book->slug . '.md');
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\Controller;
use Throwable;
class BookExportController extends Controller
{
protected $bookRepo;
protected $exportFormatter;
/**
* BookExportController constructor.
*/
public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
{
$this->bookRepo = $bookRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}
/**
* Export a book as a PDF file.
*
* @throws Throwable
*/
public function pdf(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
}
/**
* Export a book as a contained HTML file.
*
* @throws Throwable
*/
public function html(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->download()->directly($htmlContent, $bookSlug . '.html');
}
/**
* Export a book as a plain text file.
*/
public function plainText(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$textContent = $this->exportFormatter->bookToPlainText($book);
return $this->download()->directly($textContent, $bookSlug . '.txt');
}
/**
* Export a book as a markdown file.
*/
public function markdown(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$textContent = $this->exportFormatter->bookToMarkdown($book);
return $this->download()->directly($textContent, $bookSlug . '.md');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
class BookSortController extends Controller
{
protected $bookRepo;
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
}
/**
* Shows the view which allows pages to be re-ordered and sorted.
*/
public function show(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = (new BookContents($book))->getTree(false);
$this->setPageTitle(trans('entities.books_sort_named', ['bookName' => $book->getShortName()]));
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Shows the sort box for a single book.
* Used via AJAX when loading in extra books to a sort.
*/
public function showItem(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = (new BookContents($book))->getTree();
return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Sorts a book using a given mapping array.
*/
public function update(Request $request, string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->filled('sort-tree')) {
return redirect($book->getUrl());
}
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$bookContents = new BookContents($book);
$booksInvolved = $bookContents->sortUsingMap($sortMap);
// Rebuild permissions and add activity for involved books.
foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
}
return redirect($book->getUrl());
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BookshelfApiController extends ApiController
{
protected BookshelfRepo $bookshelfRepo;
public function __construct(BookshelfRepo $bookshelfRepo)
{
$this->bookshelfRepo = $bookshelfRepo;
}
/**
* Get a listing of shelves visible to the user.
*/
public function list()
{
$shelves = Bookshelf::visible();
return $this->apiListingResponse($shelves, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
]);
}
/**
* Create a new shelf in the system.
* An array of books IDs can be provided in the request. These
* will be added to the shelf in the same order as provided.
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
* If the 'image' property is null then the shelf cover image will be removed.
*
* @throws ValidationException
*/
public function create(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$requestData = $this->validate($request, $this->rules()['create']);
$bookIds = $request->get('books', []);
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
return response()->json($shelf);
}
/**
* View the details of a single shelf.
*/
public function read(string $id)
{
$shelf = Bookshelf::visible()->with([
'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
'books' => function (BelongsToMany $query) {
$query->scopes('visible')->get(['id', 'name', 'slug']);
},
])->findOrFail($id);
return response()->json($shelf);
}
/**
* Update the details of a single shelf.
* An array of books IDs can be provided in the request. These
* will be added to the shelf in the same order as provided and overwrite
* any existing book assignments.
* The cover image of a shelf can be set by sending a file via an 'image' property within a 'multipart/form-data' request.
* If the 'image' property is null then the shelf cover image will be removed.
*
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$shelf = Bookshelf::visible()->findOrFail($id);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules()['update']);
$bookIds = $request->get('books', null);
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
return response()->json($shelf);
}
/**
* Delete a single shelf.
* This will typically send the shelf to the recycle bin.
*
* @throws Exception
*/
public function delete(string $id)
{
$shelf = Bookshelf::visible()->findOrFail($id);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf);
return response('', 204);
}
protected function rules(): array
{
return [
'create' => [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'books' => ['array'],
'tags' => ['array'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
],
'update' => [
'name' => ['string', 'min:1', 'max:255'],
'description' => ['string', 'max:1000'],
'books' => ['array'],
'tags' => ['array'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
],
];
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityQueries;
use BookStack\Activity\Models\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BookshelfController extends Controller
{
protected BookshelfRepo $shelfRepo;
protected ShelfContext $shelfContext;
protected ReferenceFetcher $referenceFetcher;
public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext, ReferenceFetcher $referenceFetcher)
{
$this->shelfRepo = $shelfRepo;
$this->shelfContext = $shelfContext;
$this->referenceFetcher = $referenceFetcher;
}
/**
* Display a listing of bookshelves.
*/
public function index(Request $request)
{
$view = setting()->getForCurrentUser('bookshelves_view_type');
$listOptions = SimpleListOptions::fromRequest($request, 'bookshelves')->withSortOptions([
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
]);
$shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
$recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
$popular = $this->shelfRepo->getPopular(4);
$new = $this->shelfRepo->getRecentlyCreated(4);
$this->shelfContext->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
return view('shelves.index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'view' => $view,
'listOptions' => $listOptions,
]);
}
/**
* Show the form for creating a new bookshelf.
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
*
* @throws ValidationException
* @throws ImageUploadException
*/
public function store(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
'tags' => ['array'],
]);
$bookIds = explode(',', $request->get('books', ''));
$shelf = $this->shelfRepo->create($validated, $bookIds);
return redirect($shelf->getUrl());
}
/**
* Display the bookshelf of the given slug.
*
* @throws NotFoundException
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-view', $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
'default' => trans('common.sort_default'),
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
]);
$sort = $listOptions->getSort();
$sortedVisibleShelfBooks = $shelf->visibleBooks()
->reorder($sort === 'default' ? 'order' : $sort, $listOptions->getOrder())
->get()
->values()
->all();
View::incrementFor($shelf);
$this->shelfContext->setShelfContext($shelf->id);
$view = setting()->getForCurrentUser('bookshelf_view_type');
$this->setPageTitle($shelf->getShortName());
return view('shelves.show', [
'shelf' => $shelf,
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
'view' => $view,
'activity' => $activities->entityActivity($shelf, 20, 1),
'listOptions' => $listOptions,
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
]);
}
/**
* Show the form for editing the specified bookshelf.
*/
public function edit(string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
$books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
return view('shelves.edit', [
'shelf' => $shelf,
'books' => $books,
]);
}
/**
* Update the specified bookshelf in storage.
*
* @throws ValidationException
* @throws ImageUploadException
* @throws NotFoundException
*/
public function update(Request $request, string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'image' => array_merge(['nullable'], $this->getImageValidationRules()),
'tags' => ['array'],
]);
if ($request->has('image_reset')) {
$validated['image'] = null;
} elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
unset($validated['image']);
}
$bookIds = explode(',', $request->get('books', ''));
$shelf = $this->shelfRepo->update($shelf, $validated, $bookIds);
return redirect($shelf->getUrl());
}
/**
* Shows the page to confirm deletion.
*/
public function showDelete(string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
return view('shelves.delete', ['shelf' => $shelf]);
}
/**
* Remove the specified bookshelf from storage.
*
* @throws Exception
*/
public function destroy(string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->shelfRepo->destroy($shelf);
return redirect('/shelves');
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ChapterApiController extends ApiController
{
protected $rules = [
'create' => [
'book_id' => ['required', 'integer'],
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'max:1000'],
'tags' => ['array'],
'priority' => ['integer'],
],
'update' => [
'book_id' => ['integer'],
'name' => ['string', 'min:1', 'max:255'],
'description' => ['string', 'max:1000'],
'tags' => ['array'],
'priority' => ['integer'],
],
];
public function __construct(
protected ChapterRepo $chapterRepo
) {
}
/**
* Get a listing of chapters visible to the user.
*/
public function list()
{
$chapters = Chapter::visible();
return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority',
'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
]);
}
/**
* Create a new chapter in the system.
*/
public function create(Request $request)
{
$requestData = $this->validate($request, $this->rules['create']);
$bookId = $request->get('book_id');
$book = Book::visible()->findOrFail($bookId);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($requestData, $book);
return response()->json($chapter->load(['tags']));
}
/**
* View the details of a single chapter.
*/
public function read(string $id)
{
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
$query->scopes('visible')->get(['id', 'name', 'slug']);
}])->findOrFail($id);
return response()->json($chapter);
}
/**
* Update the details of a single chapter.
* Providing a 'book_id' property will essentially move the chapter
* into that parent element if you have permissions to do so.
*/
public function update(Request $request, string $id)
{
$requestData = $this->validate($request, $this->rules()['update']);
$chapter = Chapter::visible()->findOrFail($id);
$this->checkOwnablePermission('chapter-update', $chapter);
if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
$this->checkOwnablePermission('chapter-delete', $chapter);
try {
$this->chapterRepo->move($chapter, "book:{$requestData['book_id']}");
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
return $this->jsonError(trans('errors.selected_book_not_found'));
}
}
$updatedChapter = $this->chapterRepo->update($chapter, $requestData);
return response()->json($updatedChapter->load(['tags']));
}
/**
* Delete a chapter.
* This will typically send the chapter to the recycle bin.
*/
public function delete(string $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
return response('', 204);
}
}

View File

@@ -0,0 +1,261 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
use BookStack\Entities\Tools\HierarchyTransformer;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
class ChapterController extends Controller
{
protected ChapterRepo $chapterRepo;
protected ReferenceFetcher $referenceFetcher;
public function __construct(ChapterRepo $chapterRepo, ReferenceFetcher $referenceFetcher)
{
$this->chapterRepo = $chapterRepo;
$this->referenceFetcher = $referenceFetcher;
}
/**
* Show the form for creating a new chapter.
*/
public function create(string $bookSlug)
{
$book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
* Store a newly created chapter in storage.
*
* @throws ValidationException
*/
public function store(Request $request, string $bookSlug)
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
$book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
return redirect($chapter->getUrl());
}
/**
* Display the specified chapter.
*/
public function show(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $chapter->getVisiblePages();
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
View::incrementFor($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters.show', [
'book' => $chapter->book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'watchOptions' => new UserEntityWatchOptions(user(), $chapter),
'pages' => $pages,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($chapter),
]);
}
/**
* Show the form for editing the specified chapter.
*/
public function edit(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Update the specified chapter in storage.
*
* @throws NotFoundException
*/
public function update(Request $request, string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->chapterRepo->update($chapter, $request->all());
return redirect($chapter->getUrl());
}
/**
* Shows the page to confirm deletion of this chapter.
*
* @throws NotFoundException
*/
public function showDelete(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Remove the specified chapter from storage.
*
* @throws NotFoundException
* @throws Throwable
*/
public function destroy(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
return redirect($chapter->book->getUrl());
}
/**
* Show the page for moving a chapter.
*
* @throws NotFoundException
*/
public function showMove(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters.move', [
'chapter' => $chapter,
'book' => $chapter->book,
]);
}
/**
* Perform the move action for a chapter.
*
* @throws NotFoundException|NotifyException
*/
public function move(Request $request, string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
return redirect($chapter->getUrl());
}
try {
$this->chapterRepo->move($chapter, $entitySelection);
} catch (PermissionsException $exception) {
$this->showPermissionError();
} catch (MoveOperationException $exception) {
$this->showErrorNotification(trans('errors.selected_book_not_found'));
return redirect($chapter->getUrl('/move'));
}
return redirect($chapter->getUrl());
}
/**
* Show the view to copy a chapter.
*
* @throws NotFoundException
*/
public function showCopy(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
session()->flashInput(['name' => $chapter->name]);
return view('chapters.copy', [
'book' => $chapter->book,
'chapter' => $chapter,
]);
}
/**
* Create a copy of a chapter within the requested target destination.
*
* @throws NotFoundException
* @throws Throwable
*/
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$entitySelection = $request->get('entity_selection') ?: null;
$newParentBook = $entitySelection ? $this->chapterRepo->findParentByIdentifier($entitySelection) : $chapter->getParent();
if (is_null($newParentBook)) {
$this->showErrorNotification(trans('errors.selected_book_not_found'));
return redirect($chapter->getUrl('/copy'));
}
$this->checkOwnablePermission('chapter-create', $newParentBook);
$newName = $request->get('name') ?: $chapter->name;
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
$this->showSuccessNotification(trans('entities.chapters_copy_success'));
return redirect($chapterCopy->getUrl());
}
/**
* Convert the chapter to a book.
*/
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all');
$book = $transformer->transformChapterToBook($chapter);
return redirect($book->getUrl());
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;
class ChapterExportApiController extends ApiController
{
protected $exportFormatter;
/**
* ChapterExportController constructor.
*/
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}
/**
* Export a chapter as a PDF file.
*
* @throws Throwable
*/
public function exportPdf(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
}
/**
* Export a chapter as a contained HTML file.
*
* @throws Throwable
*/
public function exportHtml(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->download()->directly($htmlContent, $chapter->slug . '.html');
}
/**
* Export a chapter as a plain text file.
*/
public function exportPlainText(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
return $this->download()->directly($textContent, $chapter->slug . '.txt');
}
/**
* Export a chapter as a markdown file.
*/
public function exportMarkdown(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$markdown = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->download()->directly($markdown, $chapter->slug . '.md');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
use Throwable;
class ChapterExportController extends Controller
{
protected $chapterRepo;
protected $exportFormatter;
/**
* ChapterExportController constructor.
*/
public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
{
$this->chapterRepo = $chapterRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}
/**
* Exports a chapter to pdf.
*
* @throws NotFoundException
* @throws Throwable
*/
public function pdf(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
}
/**
* Export a chapter to a self-contained HTML file.
*
* @throws NotFoundException
* @throws Throwable
*/
public function html(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->download()->directly($containedHtml, $chapterSlug . '.html');
}
/**
* Export a chapter to a simple plaintext .txt file.
*
* @throws NotFoundException
*/
public function plainText(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToPlainText($chapter);
return $this->download()->directly($chapterText, $chapterSlug . '.txt');
}
/**
* Export a chapter to a simple markdown file.
*
* @throws NotFoundException
*/
public function markdown(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->download()->directly($chapterText, $chapterSlug . '.md');
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Http\Request;
class PageApiController extends ApiController
{
protected $rules = [
'create' => [
'book_id' => ['required_without:chapter_id', 'integer'],
'chapter_id' => ['required_without:book_id', 'integer'],
'name' => ['required', 'string', 'max:255'],
'html' => ['required_without:markdown', 'string'],
'markdown' => ['required_without:html', 'string'],
'tags' => ['array'],
'priority' => ['integer'],
],
'update' => [
'book_id' => ['integer'],
'chapter_id' => ['integer'],
'name' => ['string', 'min:1', 'max:255'],
'html' => ['string'],
'markdown' => ['string'],
'tags' => ['array'],
'priority' => ['integer'],
],
];
public function __construct(
protected PageRepo $pageRepo
) {
}
/**
* Get a listing of pages visible to the user.
*/
public function list()
{
$pages = Page::visible();
return $this->apiListingResponse($pages, [
'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
'draft', 'template',
'created_at', 'updated_at',
'created_by', 'updated_by', 'owned_by',
]);
}
/**
* Create a new page in the system.
*
* The ID of a parent book or chapter is required to indicate
* where this page should be located.
*
* Any HTML content provided should be kept to a single-block depth of plain HTML
* elements to remain compatible with the BookStack front-end and editors.
* Any images included via base64 data URIs will be extracted and saved as gallery
* images against the page during upload.
*/
public function create(Request $request)
{
$this->validate($request, $this->rules['create']);
if ($request->has('chapter_id')) {
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
} else {
$parent = Book::visible()->findOrFail($request->get('book_id'));
}
$this->checkOwnablePermission('page-create', $parent);
$draft = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
return response()->json($draft->forJsonDisplay());
}
/**
* View the details of a single page.
* Pages will always have HTML content. They may have markdown content
* if the markdown editor was used to last update the page.
*
* The 'html' property is the fully rendered & escaped HTML content that BookStack
* would show on page view, with page includes handled.
* The 'raw_html' property is the direct database stored HTML content, which would be
* what BookStack shows on page edit.
*
* See the "Content Security" section of these docs for security considerations when using
* the page content returned from this endpoint.
*/
public function read(string $id)
{
$page = $this->pageRepo->getById($id, []);
return response()->json($page->forJsonDisplay());
}
/**
* Update the details of a single page.
*
* See the 'create' action for details on the provided HTML/Markdown.
* Providing a 'book_id' or 'chapter_id' property will essentially move
* the page into that parent element if you have permissions to do so.
*/
public function update(Request $request, string $id)
{
$requestData = $this->validate($request, $this->rules['update']);
$page = $this->pageRepo->getById($id, []);
$this->checkOwnablePermission('page-update', $page);
$parent = null;
if ($request->has('chapter_id')) {
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
} elseif ($request->has('book_id')) {
$parent = Book::visible()->findOrFail($request->get('book_id'));
}
if ($parent && !$parent->matches($page->getParent())) {
$this->checkOwnablePermission('page-delete', $page);
try {
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
return $this->jsonError(trans('errors.selected_book_chapter_not_found'));
}
}
$updatedPage = $this->pageRepo->update($page, $requestData);
return response()->json($updatedPage->forJsonDisplay());
}
/**
* Delete a page.
* This will typically send the page to the recycle bin.
*/
public function delete(string $id)
{
$page = $this->pageRepo->getById($id, []);
$this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page);
return response('', 204);
}
}

View File

@@ -0,0 +1,447 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
class PageController extends Controller
{
public function __construct(
protected PageRepo $pageRepo,
protected ReferenceFetcher $referenceFetcher
) {
}
/**
* Show the form for creating a new page.
*
* @throws Throwable
*/
public function create(string $bookSlug, string $chapterSlug = null)
{
$parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in
if ($this->isSignedIn()) {
$draft = $this->pageRepo->getNewDraftPage($parent);
return redirect($draft->getUrl());
}
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages.guest-create', ['parent' => $parent]);
}
/**
* Create a new page as a guest user.
*
* @throws ValidationException
*/
public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
$parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$this->checkOwnablePermission('page-create', $parent);
$page = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($page, [
'name' => $request->get('name'),
]);
return redirect($page->getUrl('/edit'));
}
/**
* Show form to continue editing a draft page.
*
* @throws NotFoundException
*/
public function editDraft(Request $request, string $bookSlug, int $pageId)
{
$draft = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-create', $draft->getParent());
$editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
return view('pages.edit', $editorData->getViewData());
}
/**
* Store a new page by changing a draft into a page.
*
* @throws NotFoundException
* @throws ValidationException
*/
public function store(Request $request, string $bookSlug, int $pageId)
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
$draftPage = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-create', $draftPage->getParent());
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
return redirect($page->getUrl());
}
/**
* Display the specified page.
* If the page is not found via the slug the revisions are searched for a match.
*
* @throws NotFoundException
*/
public function show(string $bookSlug, string $pageSlug)
{
try {
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
} catch (NotFoundException $e) {
$page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
if ($page === null) {
throw $e;
}
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$pageContent = (new PageContent($page));
$page->html = $pageContent->render();
$pageNav = $pageContent->getNavigation($page->html);
$sidebarTree = (new BookContents($page->book))->getTree();
$commentTree = (new CommentTree($page));
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
View::incrementFor($page);
$this->setPageTitle($page->getShortName());
return view('pages.show', [
'page' => $page,
'book' => $page->book,
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentTree' => $commentTree,
'pageNav' => $pageNav,
'watchOptions' => new UserEntityWatchOptions(user(), $page),
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($page),
]);
}
/**
* Get page from an ajax request.
*
* @throws NotFoundException
*/
public function getPageAjax(int $pageId)
{
$page = $this->pageRepo->getById($pageId);
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
$page->makeHidden(['book']);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
*
* @throws NotFoundException
*/
public function edit(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
if ($editorData->getWarnings()) {
$this->showWarningNotification(implode("\n", $editorData->getWarnings()));
}
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
return view('pages.edit', $editorData->getViewData());
}
/**
* Update the specified page in storage.
*
* @throws ValidationException
* @throws NotFoundException
*/
public function update(Request $request, string $bookSlug, string $pageSlug)
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->update($page, $request->all());
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
*
* @throws NotFoundException
*/
public function saveDraft(Request $request, int $pageId)
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
if (!$this->isSignedIn()) {
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
}
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'warning' => implode("\n", $warnings),
'timestamp' => $draft->updated_at->timestamp,
]);
}
/**
* Redirect from a special link url which uses the page id rather than the name.
*
* @throws NotFoundException
*/
public function redirectFromLink(int $pageId)
{
$page = $this->pageRepo->getById($pageId);
return redirect($page->getUrl());
}
/**
* Show the deletion page for the specified page.
*
* @throws NotFoundException
*/
public function showDelete(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
$times_used_as_template = Book::where('default_template', '=', $page->id)->count();
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
'current' => $page,
'times_used_as_template' => $times_used_as_template,
]);
}
/**
* Show the deletion page for the specified page.
*
* @throws NotFoundException
*/
public function showDeleteDraft(string $bookSlug, int $pageId)
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
'current' => $page,
]);
}
/**
* Remove the specified page from storage.
*
* @throws NotFoundException
* @throws Throwable
*/
public function destroy(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$parent = $page->getParent();
$this->pageRepo->destroy($page);
return redirect($parent->getUrl());
}
/**
* Remove the specified draft page from storage.
*
* @throws NotFoundException
* @throws Throwable
*/
public function destroyDraft(string $bookSlug, int $pageId)
{
$page = $this->pageRepo->getById($pageId);
$book = $page->book;
$chapter = $page->chapter;
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->destroy($page);
$this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
if ($chapter && userCan('view', $chapter)) {
return redirect($chapter->getUrl());
}
return redirect($book->getUrl());
}
/**
* Show a listing of recently created pages.
*/
public function showRecentlyUpdated()
{
$visibleBelongsScope = function (BelongsTo $query) {
$query->scopes('visible');
};
$pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
->orderBy('updated_at', 'desc')
->paginate(20)
->setPath(url('/pages/recently-updated'));
$this->setPageTitle(trans('entities.recently_updated_pages'));
return view('common.detailed-listing-paginated', [
'title' => trans('entities.recently_updated_pages'),
'entities' => $pages,
'showUpdatedBy' => true,
'showPath' => true,
]);
}
/**
* Show the view to choose a new parent to move a page into.
*
* @throws NotFoundException
*/
public function showMove(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
'book' => $page->book,
'page' => $page,
]);
}
/**
* Does the action of moving the location of a page.
*
* @throws NotFoundException
* @throws Throwable
*/
public function move(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
return redirect($page->getUrl());
}
try {
$this->pageRepo->move($page, $entitySelection);
} catch (PermissionsException $exception) {
$this->showPermissionError();
} catch (Exception $exception) {
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect($page->getUrl('/move'));
}
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
*
* @throws NotFoundException
*/
public function showCopy(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages.copy', [
'book' => $page->book,
'page' => $page,
]);
}
/**
* Create a copy of a page within the requested target destination.
*
* @throws NotFoundException
* @throws Throwable
*/
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection') ?: null;
$newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
if (is_null($newParent)) {
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect($page->getUrl('/copy'));
}
$this->checkOwnablePermission('page-create', $newParent);
$newName = $request->get('name') ?: $page->name;
$pageCopy = $cloner->clonePage($page, $newParent, $newName);
$this->showSuccessNotification(trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\ApiController;
use Throwable;
class PageExportApiController extends ApiController
{
protected $exportFormatter;
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}
/**
* Export a page as a PDF file.
*
* @throws Throwable
*/
public function exportPdf(int $id)
{
$page = Page::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->pageToPdf($page);
return $this->download()->directly($pdfContent, $page->slug . '.pdf');
}
/**
* Export a page as a contained HTML file.
*
* @throws Throwable
*/
public function exportHtml(int $id)
{
$page = Page::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
return $this->download()->directly($htmlContent, $page->slug . '.html');
}
/**
* Export a page as a plain text file.
*/
public function exportPlainText(int $id)
{
$page = Page::visible()->findOrFail($id);
$textContent = $this->exportFormatter->pageToPlainText($page);
return $this->download()->directly($textContent, $page->slug . '.txt');
}
/**
* Export a page as a markdown file.
*/
public function exportMarkdown(int $id)
{
$page = Page::visible()->findOrFail($id);
$markdown = $this->exportFormatter->pageToMarkdown($page);
return $this->download()->directly($markdown, $page->slug . '.md');
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
use Throwable;
class PageExportController extends Controller
{
protected $pageRepo;
protected $exportFormatter;
/**
* PageExportController constructor.
*/
public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter)
{
$this->pageRepo = $pageRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}
/**
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf.
*
* @throws NotFoundException
* @throws Throwable
*/
public function pdf(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
$pdfContent = $this->exportFormatter->pageToPdf($page);
return $this->download()->directly($pdfContent, $pageSlug . '.pdf');
}
/**
* Export a page to a self-contained HTML file.
*
* @throws NotFoundException
* @throws Throwable
*/
public function html(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
$containedHtml = $this->exportFormatter->pageToContainedHtml($page);
return $this->download()->directly($containedHtml, $pageSlug . '.html');
}
/**
* Export a page to a simple plaintext .txt file.
*
* @throws NotFoundException
*/
public function plainText(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToPlainText($page);
return $this->download()->directly($pageText, $pageSlug . '.txt');
}
/**
* Export a page to a simple markdown .md file.
*
* @throws NotFoundException
*/
public function markdown(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToMarkdown($page);
return $this->download()->directly($pageText, $pageSlug . '.md');
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Repos\RevisionRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Ssddanbrown\HtmlDiff\Diff;
class PageRevisionController extends Controller
{
public function __construct(
protected PageRepo $pageRepo,
protected RevisionRepo $revisionRepo,
) {
}
/**
* Shows the last revisions for this page.
*
* @throws NotFoundException
*/
public function index(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
'id' => trans('entities.pages_revisions_sort_number')
]);
$revisions = $page->revisions()->select([
'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
'type', 'revision_number', 'summary',
])
->selectRaw("IF(markdown = '', false, true) as is_markdown")
->with(['page.book', 'createdBy'])
->reorder('id', $listOptions->getOrder())
->reorder('created_at', $listOptions->getOrder())
->paginate(50);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
return view('pages.revisions', [
'revisions' => $revisions,
'page' => $page,
'listOptions' => $listOptions,
]);
}
/**
* Shows a preview of a single revision.
*
* @throws NotFoundException
*/
public function show(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
throw new NotFoundException();
}
$page->fill($revision->toArray());
// TODO - Refactor PageContent so we don't need to juggle this
$page->html = $revision->html;
$page->html = (new PageContent($page))->render();
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => null,
'revision' => $revision,
]);
}
/**
* Shows the changes of a single revision.
*
* @throws NotFoundException
*/
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
throw new NotFoundException();
}
$prev = $revision->getPrevious();
$prevContent = $prev->html ?? '';
$diff = Diff::excecute($prevContent, $revision->html);
$page->fill($revision->toArray());
// TODO - Refactor PageContent so we don't need to juggle this
$page->html = $revision->html;
$page->html = (new PageContent($page))->render();
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => $diff,
'revision' => $revision,
]);
}
/**
* Restores a page using the content of the specified revision.
*
* @throws NotFoundException
*/
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $revisionId);
return redirect($page->getUrl());
}
/**
* Deletes a revision using the id of the specified revision.
*
* @throws NotFoundException
*/
public function destroy(string $bookSlug, string $pageSlug, int $revId)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {
throw new NotFoundException("Revision #{$revId} not found");
}
// Check if it's the latest revision, cannot delete the latest revision.
if (intval($page->currentRevision->id ?? null) === intval($revId)) {
$this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
return redirect($page->getUrl('/revisions'));
}
$revision->delete();
Activity::add(ActivityType::REVISION_DELETE, $revision);
return redirect($page->getUrl('/revisions'));
}
/**
* Destroys existing drafts, belonging to the current user, for the given page.
*/
public function destroyUserDraft(string $pageId)
{
$page = $this->pageRepo->getById($pageId);
$this->revisionRepo->deleteDraftsForCurrentUser($page);
return response('', 200);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
class PageTemplateController extends Controller
{
protected $pageRepo;
/**
* PageTemplateController constructor.
*/
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
}
/**
* Fetch a list of templates from the system.
*/
public function list(Request $request)
{
$page = $request->get('page', 1);
$search = $request->get('search', '');
$templates = $this->pageRepo->getTemplates(10, $page, $search);
if ($search) {
$templates->appends(['search' => $search]);
}
return view('pages.parts.template-manager-list', [
'templates' => $templates,
]);
}
/**
* Get the content of a template.
*
* @throws NotFoundException
*/
public function get(int $templateId)
{
$page = $this->pageRepo->getById($templateId);
if (!$page->template) {
throw new NotFoundException();
}
return response()->json([
'html' => $page->html,
'markdown' => $page->markdown,
]);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Http\ApiController;
use Closure;
use Illuminate\Database\Eloquent\Builder;
class RecycleBinApiController extends ApiController
{
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
return $next($request);
});
}
/**
* Get a top-level listing of the items in the recycle bin.
* The "deletable" property will reflect the main item deleted.
* For books and chapters, counts of child pages/chapters will
* be loaded within this "deletable" data.
* For chapters & pages, the parent item will be loaded within this "deletable" data.
* Requires permission to manage both system settings and permissions.
*/
public function list()
{
return $this->apiListingResponse(Deletion::query()->with('deletable'), [
'id',
'deleted_by',
'created_at',
'updated_at',
'deletable_type',
'deletable_id',
], [Closure::fromCallable([$this, 'listFormatter'])]);
}
/**
* Restore a single deletion from the recycle bin.
* Requires permission to manage both system settings and permissions.
*/
public function restore(DeletionRepo $deletionRepo, string $deletionId)
{
$restoreCount = $deletionRepo->restore(intval($deletionId));
return response()->json(['restore_count' => $restoreCount]);
}
/**
* Remove a single deletion from the recycle bin.
* Use this endpoint carefully as it will entirely remove the underlying deleted items from the system.
* Requires permission to manage both system settings and permissions.
*/
public function destroy(DeletionRepo $deletionRepo, string $deletionId)
{
$deleteCount = $deletionRepo->destroy(intval($deletionId));
return response()->json(['delete_count' => $deleteCount]);
}
/**
* Load some related details for the deletion listing.
*/
protected function listFormatter(Deletion $deletion)
{
$deletable = $deletion->deletable;
$withTrashedQuery = fn (Builder $query) => $query->withTrashed();
if ($deletable instanceof BookChild) {
$parent = $deletable->getParent();
$parent->setAttribute('type', $parent->getType());
$deletable->setRelation('parent', $parent);
}
if ($deletable instanceof Book || $deletable instanceof Chapter) {
$countsToLoad = ['pages' => $withTrashedQuery];
if ($deletable instanceof Book) {
$countsToLoad['chapters'] = $withTrashedQuery;
}
$deletable->loadCount($countsToLoad);
}
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Http\Controller;
class RecycleBinController extends Controller
{
protected string $recycleBinBaseUrl = '/settings/recycle-bin';
/**
* On each request to a method of this controller check permissions
* using a middleware closure.
*/
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
return $next($request);
});
}
/**
* Show the top-level listing for the recycle bin.
*/
public function index()
{
$deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);
$this->setPageTitle(trans('settings.recycle_bin'));
return view('settings.recycle-bin.index', [
'deletions' => $deletions,
]);
}
/**
* Show the page to confirm a restore of the deletion of the given id.
*/
public function showRestore(string $id)
{
/** @var Deletion $deletion */
$deletion = Deletion::query()->findOrFail($id);
// Walk the parent chain to find any cascading parent deletions
$currentDeletable = $deletion->deletable;
$searching = true;
while ($searching && $currentDeletable instanceof Entity) {
$parent = $currentDeletable->getParent();
if ($parent && $parent->trashed()) {
$currentDeletable = $parent;
} else {
$searching = false;
}
}
/** @var ?Deletion $parentDeletion */
$parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first();
return view('settings.recycle-bin.restore', [
'deletion' => $deletion,
'parentDeletion' => $parentDeletion,
]);
}
/**
* Restore the element attached to the given deletion.
*
* @throws \Exception
*/
public function restore(DeletionRepo $deletionRepo, string $id)
{
$restoreCount = $deletionRepo->restore((int) $id);
$this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
return redirect($this->recycleBinBaseUrl);
}
/**
* Show the page to confirm a Permanent deletion of the element attached to the deletion of the given id.
*/
public function showDestroy(string $id)
{
/** @var Deletion $deletion */
$deletion = Deletion::query()->findOrFail($id);
return view('settings.recycle-bin.destroy', [
'deletion' => $deletion,
]);
}
/**
* Permanently delete the content associated with the given deletion.
*
* @throws \Exception
*/
public function destroy(DeletionRepo $deletionRepo, string $id)
{
$deleteCount = $deletionRepo->destroy((int) $id);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
return redirect($this->recycleBinBaseUrl);
}
/**
* Empty out the recycle bin.
*
* @throws \Exception
*/
public function empty()
{
$deleteCount = (new TrashCan())->empty();
$this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
return redirect($this->recycleBinBaseUrl);
}
}