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\BookRepo;
8 use BookStack\Entities\Repos\EntityRepo;
9 use BookStack\Entities\ExportService;
10 use BookStack\Exceptions\ImageUploadException;
11 use BookStack\Exceptions\NotFoundException;
12 use BookStack\Exceptions\NotifyException;
13 use BookStack\Uploads\ImageRepo;
14 use Illuminate\Contracts\View\Factory;
15 use Illuminate\Http\RedirectResponse;
16 use Illuminate\Http\Request;
17 use Illuminate\Http\Response;
18 use Illuminate\Routing\Redirector;
19 use Illuminate\Validation\ValidationException;
20 use Illuminate\View\View;
24 class BookController extends Controller
29 protected $entityContextManager;
33 * BookController constructor.
34 * @param BookRepo $bookRepo
35 * @param UserRepo $userRepo
36 * @param EntityContextManager $entityContextManager
37 * @param ImageRepo $imageRepo
39 public function __construct(
42 EntityContextManager $entityContextManager,
45 $this->bookRepo = $bookRepo;
46 $this->userRepo = $userRepo;
47 $this->entityContextManager = $entityContextManager;
48 $this->imageRepo = $imageRepo;
49 parent::__construct();
53 * Display a listing of the book.
56 public function index()
58 $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
59 $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
60 $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
62 'name' => trans('common.sort_name'),
63 'created_at' => trans('common.sort_created_at'),
64 'updated_at' => trans('common.sort_updated_at'),
67 $books = $this->bookRepo->getAllPaginated('book', 18, $sort, $order);
68 $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed('book', 4, 0) : false;
69 $popular = $this->bookRepo->getPopular('book', 4, 0);
70 $new = $this->bookRepo->getRecentlyCreated('book', 4, 0);
72 $this->entityContextManager->clearShelfContext();
74 $this->setPageTitle(trans('entities.books'));
75 return view('books.index', [
77 'recents' => $recents,
78 'popular' => $popular,
83 'sortOptions' => $sortOptions,
88 * Show the form for creating a new book.
89 * @param string $shelfSlug
91 * @throws NotFoundException
93 public function create(string $shelfSlug = null)
96 if ($shelfSlug !== null) {
97 $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
98 $this->checkOwnablePermission('bookshelf-update', $bookshelf);
101 $this->checkPermission('book-create-all');
102 $this->setPageTitle(trans('entities.books_create'));
103 return view('books.create', [
104 'bookshelf' => $bookshelf
109 * Store a newly created book in storage.
111 * @param Request $request
112 * @param string $shelfSlug
114 * @throws NotFoundException
115 * @throws ImageUploadException
116 * @throws ValidationException
118 public function store(Request $request, string $shelfSlug = null)
120 $this->checkPermission('book-create-all');
121 $this->validate($request, [
122 'name' => 'required|string|max:255',
123 'description' => 'string|max:1000',
124 'image' => $this->imageRepo->getImageValidationRules(),
128 if ($shelfSlug !== null) {
129 $bookshelf = $this->bookRepo->getEntityBySlug('bookshelf', $shelfSlug);
130 $this->checkOwnablePermission('bookshelf-update', $bookshelf);
133 $book = $this->bookRepo->createFromInput('book', $request->all());
134 $this->bookUpdateActions($book, $request);
135 Activity::add($book, 'book_create', $book->id);
138 $this->bookRepo->appendBookToShelf($bookshelf, $book);
139 Activity::add($bookshelf, 'bookshelf_update');
142 return redirect($book->getUrl());
146 * Display the specified book.
147 * @param Request $request
148 * @param string $slug
150 * @throws NotFoundException
152 public function show(Request $request, string $slug)
154 $book = $this->bookRepo->getBySlug($slug);
155 $this->checkOwnablePermission('book-view', $book);
157 $bookChildren = $this->bookRepo->getBookChildren($book);
160 if ($request->has('shelf')) {
161 $this->entityContextManager->setShelfContext(intval($request->get('shelf')));
164 $this->setPageTitle($book->getShortName());
165 return view('books.show', [
168 'bookChildren' => $bookChildren,
169 'activity' => Activity::entityActivity($book, 20, 1)
174 * Show the form for editing the specified book.
175 * @param string $slug
177 * @throws NotFoundException
179 public function edit(string $slug)
181 $book = $this->bookRepo->getBySlug($slug);
182 $this->checkOwnablePermission('book-update', $book);
183 $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
184 return view('books.edit', ['book' => $book, 'current' => $book]);
188 * Update the specified book in storage.
189 * @param Request $request
190 * @param string $slug
192 * @throws ImageUploadException
193 * @throws NotFoundException
194 * @throws ValidationException
196 public function update(Request $request, string $slug)
198 $book = $this->bookRepo->getBySlug($slug);
199 $this->checkOwnablePermission('book-update', $book);
200 $this->validate($request, [
201 'name' => 'required|string|max:255',
202 'description' => 'string|max:1000',
203 'image' => $this->imageRepo->getImageValidationRules(),
206 $book = $this->bookRepo->updateFromInput('book', $book, $request->all());
207 $this->bookUpdateActions($book, $request);
209 Activity::add($book, 'book_update', $book->id);
211 return redirect($book->getUrl());
215 * Shows the page to confirm deletion
216 * @param string $bookSlug
218 * @throws NotFoundException
220 public function showDelete(string $bookSlug)
222 $book = $this->bookRepo->getBySlug($bookSlug);
223 $this->checkOwnablePermission('book-delete', $book);
224 $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
225 return view('books.delete', ['book' => $book, 'current' => $book]);
229 * Shows the view which allows pages to be re-ordered and sorted.
230 * @param string $bookSlug
232 * @throws NotFoundException
234 public function sort(string $bookSlug)
236 $book = $this->bookRepo->getBySlug($bookSlug);
237 $this->checkOwnablePermission('book-update', $book);
239 $bookChildren = $this->bookRepo->getBookChildren($book, true);
241 $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
242 return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
246 * Shows the sort box for a single book.
247 * Used via AJAX when loading in extra books to a sort.
248 * @param string $bookSlug
249 * @return Factory|View
250 * @throws NotFoundException
252 public function getSortItem(string $bookSlug)
254 $book = $this->bookRepo->getBySlug($bookSlug);
255 $bookChildren = $this->bookRepo->getBookChildren($book);
256 return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
260 * Saves an array of sort mapping to pages and chapters.
261 * @param Request $request
262 * @param string $bookSlug
263 * @return RedirectResponse|Redirector
264 * @throws NotFoundException
266 public function saveSort(Request $request, string $bookSlug)
268 $book = $this->bookRepo->getBySlug($bookSlug);
269 $this->checkOwnablePermission('book-update', $book);
271 // Return if no map sent
272 if (!$request->filled('sort-tree')) {
273 return redirect($book->getUrl());
276 // Sort pages and chapters
277 $sortMap = collect(json_decode($request->get('sort-tree')));
278 $bookIdsInvolved = collect([$book->id]);
280 // Load models into map
281 $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
282 $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
283 $mapItem->model = $this->bookRepo->getById($mapItem->type, $mapItem->id);
284 // Store source and target books
285 $bookIdsInvolved->push(intval($mapItem->model->book_id));
286 $bookIdsInvolved->push(intval($mapItem->book));
289 // Get the books involved in the sort
290 $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
291 $booksInvolved = $this->bookRepo->getManyById('book', $bookIdsInvolved, false, true);
292 // Throw permission error if invalid ids or inaccessible books given.
293 if (count($bookIdsInvolved) !== count($booksInvolved)) {
294 $this->showPermissionError();
296 // Check permissions of involved books
297 $booksInvolved->each(function (Book $book) {
298 $this->checkOwnablePermission('book-update', $book);
302 $sortMap->each(function ($mapItem) {
303 $model = $mapItem->model;
305 $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
306 $bookChanged = intval($model->book_id) !== intval($mapItem->book);
307 $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
310 $this->bookRepo->changeBook($mapItem->type, $mapItem->book, $model);
312 if ($chapterChanged) {
313 $model->chapter_id = intval($mapItem->parentChapter);
316 if ($priorityChanged) {
317 $model->priority = intval($mapItem->sort);
322 // Rebuild permissions and add activity for involved books.
323 $booksInvolved->each(function (Book $book) {
324 $this->bookRepo->buildJointPermissionsForBook($book);
325 Activity::add($book, 'book_sort', $book->id);
328 return redirect($book->getUrl());
332 * Remove the specified book from storage.
333 * @param string $bookSlug
335 * @throws NotFoundException
337 * @throws NotifyException
339 public function destroy(string $bookSlug)
341 $book = $this->bookRepo->getBySlug($bookSlug);
342 $this->checkOwnablePermission('book-delete', $book);
343 Activity::addMessage('book_delete', 0, $book->name);
346 $this->imageRepo->destroyImage($book->cover);
348 $this->bookRepo->destroyBook($book);
350 return redirect('/books');
354 * Show the Restrictions view.
355 * @param string $bookSlug
356 * @return Factory|View
357 * @throws NotFoundException
359 public function showPermissions(string $bookSlug)
361 $book = $this->bookRepo->getBySlug($bookSlug);
362 $this->checkOwnablePermission('restrictions-manage', $book);
363 $roles = $this->userRepo->getRestrictableRoles();
364 return view('books.permissions', [
371 * Set the restrictions for this book.
372 * @param Request $request
373 * @param string $bookSlug
374 * @return RedirectResponse|Redirector
375 * @throws NotFoundException
378 public function permissions(Request $request, string $bookSlug)
380 $book = $this->bookRepo->getBySlug($bookSlug);
381 $this->checkOwnablePermission('restrictions-manage', $book);
382 $this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
383 session()->flash('success', trans('entities.books_permissions_updated'));
384 return redirect($book->getUrl());
388 * Common actions to run on book update.
389 * Handles updating the cover image.
391 * @param Request $request
392 * @throws ImageUploadException
394 protected function bookUpdateActions(Book $book, Request $request)
396 // Update the cover image if in request
397 if ($request->has('image')) {
398 $this->imageRepo->destroyImage($book->cover);
399 $newImage = $request->file('image');
400 $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
401 $book->image_id = $image->id;
405 if ($request->has('image_reset')) {
406 $this->imageRepo->destroyImage($book->cover);