1 <?php namespace BookStack\Http\Controllers;
4 use BookStack\Auth\UserRepo;
5 use BookStack\Entities\Book;
6 use BookStack\Entities\EntityContextManager;
7 use BookStack\Entities\Repos\EntityRepo;
8 use BookStack\Entities\ExportService;
9 use BookStack\Uploads\ImageRepo;
10 use Illuminate\Http\Request;
11 use Illuminate\Http\Response;
14 class BookController extends Controller
17 protected $entityRepo;
19 protected $exportService;
20 protected $entityContextManager;
24 * BookController constructor.
25 * @param EntityRepo $entityRepo
26 * @param UserRepo $userRepo
27 * @param ExportService $exportService
28 * @param EntityContextManager $entityContextManager
29 * @param ImageRepo $imageRepo
31 public function __construct(
32 EntityRepo $entityRepo,
34 ExportService $exportService,
35 EntityContextManager $entityContextManager,
38 $this->entityRepo = $entityRepo;
39 $this->userRepo = $userRepo;
40 $this->exportService = $exportService;
41 $this->entityContextManager = $entityContextManager;
42 $this->imageRepo = $imageRepo;
43 parent::__construct();
47 * Display a listing of the book.
50 public function index()
52 $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
53 $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
54 $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
56 'name' => trans('common.sort_name'),
57 'created_at' => trans('common.sort_created_at'),
58 'updated_at' => trans('common.sort_updated_at'),
61 $books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
62 $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
63 $popular = $this->entityRepo->getPopular('book', 4, 0);
64 $new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
66 $this->entityContextManager->clearShelfContext();
68 $this->setPageTitle(trans('entities.books'));
69 return view('books.index', [
71 'recents' => $recents,
72 'popular' => $popular,
77 'sortOptions' => $sortOptions,
82 * Show the form for creating a new book.
83 * @param string $shelfSlug
85 * @throws \BookStack\Exceptions\NotFoundException
87 public function create(string $shelfSlug = null)
90 if ($shelfSlug !== null) {
91 $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
92 $this->checkOwnablePermission('bookshelf-update', $bookshelf);
95 $this->checkPermission('book-create-all');
96 $this->setPageTitle(trans('entities.books_create'));
97 return view('books.create', [
98 'bookshelf' => $bookshelf
103 * Store a newly created book in storage.
105 * @param Request $request
106 * @param string $shelfSlug
108 * @throws \BookStack\Exceptions\NotFoundException
109 * @throws \BookStack\Exceptions\ImageUploadException
111 public function store(Request $request, string $shelfSlug = null)
113 $this->checkPermission('book-create-all');
114 $this->validate($request, [
115 'name' => 'required|string|max:255',
116 'description' => 'string|max:1000',
117 'image' => $this->imageRepo->getImageValidationRules(),
121 if ($shelfSlug !== null) {
122 $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
123 $this->checkOwnablePermission('bookshelf-update', $bookshelf);
126 $book = $this->entityRepo->createFromInput('book', $request->all());
127 $this->bookUpdateActions($book, $request);
128 Activity::add($book, 'book_create', $book->id);
131 $this->entityRepo->appendBookToShelf($bookshelf, $book);
132 Activity::add($bookshelf, 'bookshelf_update');
135 return redirect($book->getUrl());
139 * Display the specified book.
140 * @param Request $request
141 * @param string $slug
143 * @throws \BookStack\Exceptions\NotFoundException
145 public function show(Request $request, string $slug)
147 $book = $this->entityRepo->getBySlug('book', $slug);
148 $this->checkOwnablePermission('book-view', $book);
150 $bookChildren = $this->entityRepo->getBookChildren($book);
153 if ($request->has('shelf')) {
154 $this->entityContextManager->setShelfContext(intval($request->get('shelf')));
157 $this->setPageTitle($book->getShortName());
158 return view('books.show', [
161 'bookChildren' => $bookChildren,
162 'activity' => Activity::entityActivity($book, 20, 1)
167 * Show the form for editing the specified book.
171 public function edit($slug)
173 $book = $this->entityRepo->getBySlug('book', $slug);
174 $this->checkOwnablePermission('book-update', $book);
175 $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
176 return view('books.edit', ['book' => $book, 'current' => $book]);
180 * Update the specified book in storage.
181 * @param Request $request
184 * @throws \BookStack\Exceptions\ImageUploadException
185 * @throws \BookStack\Exceptions\NotFoundException
187 public function update(Request $request, string $slug)
189 $book = $this->entityRepo->getBySlug('book', $slug);
190 $this->checkOwnablePermission('book-update', $book);
191 $this->validate($request, [
192 'name' => 'required|string|max:255',
193 'description' => 'string|max:1000',
194 'image' => $this->imageRepo->getImageValidationRules(),
197 $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
198 $this->bookUpdateActions($book, $request);
200 Activity::add($book, 'book_update', $book->id);
202 return redirect($book->getUrl());
206 * Shows the page to confirm deletion
208 * @return \Illuminate\View\View
210 public function showDelete($bookSlug)
212 $book = $this->entityRepo->getBySlug('book', $bookSlug);
213 $this->checkOwnablePermission('book-delete', $book);
214 $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
215 return view('books.delete', ['book' => $book, 'current' => $book]);
219 * Shows the view which allows pages to be re-ordered and sorted.
220 * @param string $bookSlug
221 * @return \Illuminate\View\View
222 * @throws \BookStack\Exceptions\NotFoundException
224 public function sort($bookSlug)
226 $book = $this->entityRepo->getBySlug('book', $bookSlug);
227 $this->checkOwnablePermission('book-update', $book);
229 $bookChildren = $this->entityRepo->getBookChildren($book, true);
231 $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
232 return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
236 * Shows the sort box for a single book.
237 * Used via AJAX when loading in extra books to a sort.
239 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
241 public function getSortItem($bookSlug)
243 $book = $this->entityRepo->getBySlug('book', $bookSlug);
244 $bookChildren = $this->entityRepo->getBookChildren($book);
245 return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
249 * Saves an array of sort mapping to pages and chapters.
250 * @param Request $request
251 * @param string $bookSlug
252 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
253 * @throws \BookStack\Exceptions\NotFoundException
255 public function saveSort(Request $request, string $bookSlug)
257 $book = $this->entityRepo->getBySlug('book', $bookSlug);
258 $this->checkOwnablePermission('book-update', $book);
260 // Return if no map sent
261 if (!$request->filled('sort-tree')) {
262 return redirect($book->getUrl());
265 // Sort pages and chapters
266 $sortMap = collect(json_decode($request->get('sort-tree')));
267 $bookIdsInvolved = collect([$book->id]);
269 // Load models into map
270 $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
271 $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
272 $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
273 // Store source and target books
274 $bookIdsInvolved->push(intval($mapItem->model->book_id));
275 $bookIdsInvolved->push(intval($mapItem->book));
278 // Get the books involved in the sort
279 $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
280 $booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
281 // Throw permission error if invalid ids or inaccessible books given.
282 if (count($bookIdsInvolved) !== count($booksInvolved)) {
283 $this->showPermissionError();
285 // Check permissions of involved books
286 $booksInvolved->each(function (Book $book) {
287 $this->checkOwnablePermission('book-update', $book);
291 $sortMap->each(function ($mapItem) {
292 $model = $mapItem->model;
294 $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
295 $bookChanged = intval($model->book_id) !== intval($mapItem->book);
296 $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
299 $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
301 if ($chapterChanged) {
302 $model->chapter_id = intval($mapItem->parentChapter);
305 if ($priorityChanged) {
306 $model->priority = intval($mapItem->sort);
311 // Rebuild permissions and add activity for involved books.
312 $booksInvolved->each(function (Book $book) {
313 $this->entityRepo->buildJointPermissionsForBook($book);
314 Activity::add($book, 'book_sort', $book->id);
317 return redirect($book->getUrl());
321 * Remove the specified book from storage.
325 public function destroy($bookSlug)
327 $book = $this->entityRepo->getBySlug('book', $bookSlug);
328 $this->checkOwnablePermission('book-delete', $book);
329 Activity::addMessage('book_delete', 0, $book->name);
332 $this->imageRepo->destroyImage($book->cover);
334 $this->entityRepo->destroyBook($book);
336 return redirect('/books');
340 * Show the Restrictions view.
342 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
344 public function showPermissions($bookSlug)
346 $book = $this->entityRepo->getBySlug('book', $bookSlug);
347 $this->checkOwnablePermission('restrictions-manage', $book);
348 $roles = $this->userRepo->getRestrictableRoles();
349 return view('books.permissions', [
356 * Set the restrictions for this book.
357 * @param Request $request
358 * @param string $bookSlug
359 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
360 * @throws \BookStack\Exceptions\NotFoundException
363 public function permissions(Request $request, string $bookSlug)
365 $book = $this->entityRepo->getBySlug('book', $bookSlug);
366 $this->checkOwnablePermission('restrictions-manage', $book);
367 $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
368 session()->flash('success', trans('entities.books_permissions_updated'));
369 return redirect($book->getUrl());
373 * Export a book as a PDF file.
374 * @param string $bookSlug
377 public function exportPdf($bookSlug)
379 $book = $this->entityRepo->getBySlug('book', $bookSlug);
380 $pdfContent = $this->exportService->bookToPdf($book);
381 return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
385 * Export a book as a contained HTML file.
386 * @param string $bookSlug
389 public function exportHtml($bookSlug)
391 $book = $this->entityRepo->getBySlug('book', $bookSlug);
392 $htmlContent = $this->exportService->bookToContainedHtml($book);
393 return $this->downloadResponse($htmlContent, $bookSlug . '.html');
397 * Export a book as a plain text file.
401 public function exportPlainText($bookSlug)
403 $book = $this->entityRepo->getBySlug('book', $bookSlug);
404 $textContent = $this->exportService->bookToPlainText($book);
405 return $this->downloadResponse($textContent, $bookSlug . '.txt');
409 * Common actions to run on book update.
410 * Handles updating the cover image.
412 * @param Request $request
413 * @throws \BookStack\Exceptions\ImageUploadException
415 protected function bookUpdateActions(Book $book, Request $request)
417 // Update the cover image if in request
418 if ($request->has('image')) {
419 $this->imageRepo->destroyImage($book->cover);
420 $newImage = $request->file('image');
421 $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
422 $book->image_id = $image->id;
426 if ($request->has('image_reset')) {
427 $this->imageRepo->destroyImage($book->cover);