1 <?php namespace BookStack\Http\Controllers;
4 use BookStack\Auth\UserRepo;
5 use BookStack\Entities\Book;
6 use BookStack\Entities\Bookshelf;
7 use BookStack\Entities\EntityContextManager;
8 use BookStack\Entities\Repos\BookRepo;
9 use BookStack\Exceptions\ImageUploadException;
10 use BookStack\Exceptions\NotFoundException;
11 use BookStack\Exceptions\NotifyException;
12 use BookStack\Uploads\ImageRepo;
13 use Illuminate\Contracts\View\Factory;
14 use Illuminate\Http\RedirectResponse;
15 use Illuminate\Http\Request;
16 use Illuminate\Http\Response;
17 use Illuminate\Routing\Redirector;
18 use Illuminate\Validation\ValidationException;
19 use Illuminate\View\View;
23 class BookController extends Controller
28 protected $entityContextManager;
32 * BookController constructor.
33 * @param BookRepo $bookRepo
34 * @param UserRepo $userRepo
35 * @param EntityContextManager $entityContextManager
36 * @param ImageRepo $imageRepo
38 public function __construct(
41 EntityContextManager $entityContextManager,
44 $this->bookRepo = $bookRepo;
45 $this->userRepo = $userRepo;
46 $this->entityContextManager = $entityContextManager;
47 $this->imageRepo = $imageRepo;
48 parent::__construct();
52 * Display a listing of the book.
55 public function index()
57 $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
58 $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
59 $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
61 $books = $this->bookRepo->getAllPaginated('book', 18, $sort, $order);
62 $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed('book', 4, 0) : false;
63 $popular = $this->bookRepo->getPopular('book', 4, 0);
64 $new = $this->bookRepo->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,
81 * Show the form for creating a new book.
82 * @param string $shelfSlug
84 * @throws NotFoundException
86 public function create(string $shelfSlug = null)
89 if ($shelfSlug !== null) {
90 $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
91 $this->checkOwnablePermission('bookshelf-update', $bookshelf);
94 $this->checkPermission('book-create-all');
95 $this->setPageTitle(trans('entities.books_create'));
96 return view('books.create', [
97 'bookshelf' => $bookshelf
102 * Store a newly created book in storage.
104 * @param Request $request
105 * @param string $shelfSlug
107 * @throws NotFoundException
108 * @throws ImageUploadException
109 * @throws ValidationException
112 public function store(Request $request, string $shelfSlug = null)
114 $this->checkPermission('book-create-all');
115 $this->validate($request, [
116 'name' => 'required|string|max:255',
117 'description' => 'string|max:1000',
118 'image' => $this->imageRepo->getImageValidationRules(),
122 if ($shelfSlug !== null) {
123 /** @var Bookshelf $bookshelf */
124 $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
125 $this->checkOwnablePermission('bookshelf-update', $bookshelf);
128 /** @var Book $book */
129 $book = $this->bookRepo->createFromInput('book', $request->all());
130 $this->bookUpdateActions($book, $request);
131 Activity::add($book, 'book_create', $book->id);
134 $bookshelf->appendBook($book);
135 Activity::add($bookshelf, 'bookshelf_update');
138 return redirect($book->getUrl());
142 * Display the specified book.
143 * @param Request $request
144 * @param string $slug
146 * @throws NotFoundException
148 public function show(Request $request, string $slug)
150 $book = $this->bookRepo->getBySlug($slug);
151 $this->checkOwnablePermission('book-view', $book);
153 $bookChildren = $this->bookRepo->getBookChildren($book);
156 if ($request->has('shelf')) {
157 $this->entityContextManager->setShelfContext(intval($request->get('shelf')));
160 $this->setPageTitle($book->getShortName());
161 return view('books.show', [
164 'bookChildren' => $bookChildren,
165 'activity' => Activity::entityActivity($book, 20, 1)
170 * Show the form for editing the specified book.
171 * @param string $slug
173 * @throws NotFoundException
175 public function edit(string $slug)
177 $book = $this->bookRepo->getBySlug($slug);
178 $this->checkOwnablePermission('book-update', $book);
179 $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
180 return view('books.edit', ['book' => $book, 'current' => $book]);
184 * Update the specified book in storage.
185 * @param Request $request
186 * @param string $slug
188 * @throws ImageUploadException
189 * @throws NotFoundException
190 * @throws ValidationException
193 public function update(Request $request, string $slug)
195 $book = $this->bookRepo->getBySlug($slug);
196 $this->checkOwnablePermission('book-update', $book);
197 $this->validate($request, [
198 'name' => 'required|string|max:255',
199 'description' => 'string|max:1000',
200 'image' => $this->imageRepo->getImageValidationRules(),
203 $book = $this->bookRepo->updateFromInput('book', $book, $request->all());
204 $this->bookUpdateActions($book, $request);
206 Activity::add($book, 'book_update', $book->id);
208 return redirect($book->getUrl());
212 * Shows the page to confirm deletion
213 * @param string $bookSlug
215 * @throws NotFoundException
217 public function showDelete(string $bookSlug)
219 $book = $this->bookRepo->getBySlug($bookSlug);
220 $this->checkOwnablePermission('book-delete', $book);
221 $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
222 return view('books.delete', ['book' => $book, 'current' => $book]);
226 * Shows the view which allows pages to be re-ordered and sorted.
227 * @param string $bookSlug
229 * @throws NotFoundException
231 public function sort(string $bookSlug)
233 $book = $this->bookRepo->getBySlug($bookSlug);
234 $this->checkOwnablePermission('book-update', $book);
236 $bookChildren = $this->bookRepo->getBookChildren($book, true);
238 $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
239 return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
243 * Shows the sort box for a single book.
244 * Used via AJAX when loading in extra books to a sort.
245 * @param string $bookSlug
246 * @return Factory|View
247 * @throws NotFoundException
249 public function getSortItem(string $bookSlug)
251 $book = $this->bookRepo->getBySlug($bookSlug);
252 $bookChildren = $this->bookRepo->getBookChildren($book);
253 return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
257 * Saves an array of sort mapping to pages and chapters.
258 * @param Request $request
259 * @param string $bookSlug
260 * @return RedirectResponse|Redirector
261 * @throws NotFoundException
263 public function saveSort(Request $request, string $bookSlug)
265 $book = $this->bookRepo->getBySlug($bookSlug);
266 $this->checkOwnablePermission('book-update', $book);
268 // Return if no map sent
269 if (!$request->filled('sort-tree')) {
270 return redirect($book->getUrl());
273 // Sort pages and chapters
274 $sortMap = collect(json_decode($request->get('sort-tree')));
275 $bookIdsInvolved = collect([$book->id]);
277 // Load models into map
278 $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
279 $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
280 $mapItem->model = $this->bookRepo->getById($mapItem->type, $mapItem->id);
281 // Store source and target books
282 $bookIdsInvolved->push(intval($mapItem->model->book_id));
283 $bookIdsInvolved->push(intval($mapItem->book));
286 // Get the books involved in the sort
287 $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
288 $booksInvolved = $this->bookRepo->getManyById('book', $bookIdsInvolved, false, true);
289 // Throw permission error if invalid ids or inaccessible books given.
290 if (count($bookIdsInvolved) !== count($booksInvolved)) {
291 $this->showPermissionError();
293 // Check permissions of involved books
294 $booksInvolved->each(function (Book $book) {
295 $this->checkOwnablePermission('book-update', $book);
299 $sortMap->each(function ($mapItem) {
300 $model = $mapItem->model;
302 $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
303 $bookChanged = intval($model->book_id) !== intval($mapItem->book);
304 $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
307 $this->bookRepo->changeBook($mapItem->type, $mapItem->book, $model);
309 if ($chapterChanged) {
310 $model->chapter_id = intval($mapItem->parentChapter);
313 if ($priorityChanged) {
314 $model->priority = intval($mapItem->sort);
319 // Rebuild permissions and add activity for involved books.
320 $booksInvolved->each(function (Book $book) {
321 $this->bookRepo->buildJointPermissionsForBook($book);
322 Activity::add($book, 'book_sort', $book->id);
325 return redirect($book->getUrl());
329 * Remove the specified book from storage.
330 * @param string $bookSlug
332 * @throws NotFoundException
334 * @throws NotifyException
336 public function destroy(string $bookSlug)
338 $book = $this->bookRepo->getBySlug($bookSlug);
339 $this->checkOwnablePermission('book-delete', $book);
340 Activity::addMessage('book_delete', $book->name);
343 $this->imageRepo->destroyImage($book->cover);
345 $this->bookRepo->destroyBook($book);
347 return redirect('/books');
351 * Show the Restrictions view.
352 * @param string $bookSlug
353 * @return Factory|View
354 * @throws NotFoundException
356 public function showPermissions(string $bookSlug)
358 $book = $this->bookRepo->getBySlug($bookSlug);
359 $this->checkOwnablePermission('restrictions-manage', $book);
360 $roles = $this->userRepo->getRestrictableRoles();
361 return view('books.permissions', [
368 * Set the restrictions for this book.
369 * @param Request $request
370 * @param string $bookSlug
371 * @return RedirectResponse|Redirector
372 * @throws NotFoundException
375 public function permissions(Request $request, string $bookSlug)
377 $book = $this->bookRepo->getBySlug($bookSlug);
378 $this->checkOwnablePermission('restrictions-manage', $book);
379 $this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
380 $this->showSuccessNotification(trans('entities.books_permissions_updated'));
381 return redirect($book->getUrl());
385 * Common actions to run on book update.
386 * Handles updating the cover image.
388 * @param Request $request
389 * @throws ImageUploadException
391 protected function bookUpdateActions(Book $book, Request $request)
393 // Update the cover image if in request
394 if ($request->has('image')) {
395 $this->imageRepo->destroyImage($book->cover);
396 $newImage = $request->file('image');
397 $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
398 $book->image_id = $image->id;
402 if ($request->has('image_reset')) {
403 $this->imageRepo->destroyImage($book->cover);