]> BookStack Code Mirror - bookstack/blob - app/Entities/Controllers/BookController.php
Perms: Added transactions around permission effecting actions
[bookstack] / app / Entities / Controllers / BookController.php
1 <?php
2
3 namespace BookStack\Entities\Controllers;
4
5 use BookStack\Activity\ActivityQueries;
6 use BookStack\Activity\ActivityType;
7 use BookStack\Activity\Models\View;
8 use BookStack\Activity\Tools\UserEntityWatchOptions;
9 use BookStack\Entities\Queries\BookQueries;
10 use BookStack\Entities\Queries\BookshelfQueries;
11 use BookStack\Entities\Repos\BookRepo;
12 use BookStack\Entities\Tools\BookContents;
13 use BookStack\Entities\Tools\Cloner;
14 use BookStack\Entities\Tools\HierarchyTransformer;
15 use BookStack\Entities\Tools\ShelfContext;
16 use BookStack\Exceptions\ImageUploadException;
17 use BookStack\Exceptions\NotFoundException;
18 use BookStack\Facades\Activity;
19 use BookStack\Http\Controller;
20 use BookStack\References\ReferenceFetcher;
21 use BookStack\Util\DatabaseTransaction;
22 use BookStack\Util\SimpleListOptions;
23 use Illuminate\Http\Request;
24 use Illuminate\Validation\ValidationException;
25 use Throwable;
26
27 class BookController extends Controller
28 {
29     public function __construct(
30         protected ShelfContext $shelfContext,
31         protected BookRepo $bookRepo,
32         protected BookQueries $queries,
33         protected BookshelfQueries $shelfQueries,
34         protected ReferenceFetcher $referenceFetcher,
35     ) {
36     }
37
38     /**
39      * Display a listing of the book.
40      */
41     public function index(Request $request)
42     {
43         $view = setting()->getForCurrentUser('books_view_type');
44         $listOptions = SimpleListOptions::fromRequest($request, 'books')->withSortOptions([
45             'name' => trans('common.sort_name'),
46             'created_at' => trans('common.sort_created_at'),
47             'updated_at' => trans('common.sort_updated_at'),
48         ]);
49
50         $books = $this->queries->visibleForListWithCover()
51             ->orderBy($listOptions->getSort(), $listOptions->getOrder())
52             ->paginate(18);
53         $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false;
54         $popular = $this->queries->popularForList()->take(4)->get();
55         $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get();
56
57         $this->shelfContext->clearShelfContext();
58
59         $this->setPageTitle(trans('entities.books'));
60
61         return view('books.index', [
62             'books'   => $books,
63             'recents' => $recents,
64             'popular' => $popular,
65             'new'     => $new,
66             'view'    => $view,
67             'listOptions' => $listOptions,
68         ]);
69     }
70
71     /**
72      * Show the form for creating a new book.
73      */
74     public function create(?string $shelfSlug = null)
75     {
76         $this->checkPermission('book-create-all');
77
78         $bookshelf = null;
79         if ($shelfSlug !== null) {
80             $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
81             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
82         }
83
84         $this->setPageTitle(trans('entities.books_create'));
85
86         return view('books.create', [
87             'bookshelf' => $bookshelf,
88         ]);
89     }
90
91     /**
92      * Store a newly created book in storage.
93      *
94      * @throws ImageUploadException
95      * @throws ValidationException
96      */
97     public function store(Request $request, ?string $shelfSlug = null)
98     {
99         $this->checkPermission('book-create-all');
100         $validated = $this->validate($request, [
101             'name'                => ['required', 'string', 'max:255'],
102             'description_html'    => ['string', 'max:2000'],
103             'image'               => array_merge(['nullable'], $this->getImageValidationRules()),
104             'tags'                => ['array'],
105             'default_template_id' => ['nullable', 'integer'],
106         ]);
107
108         $bookshelf = null;
109         if ($shelfSlug !== null) {
110             $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
111             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
112         }
113
114         $book = $this->bookRepo->create($validated);
115
116         if ($bookshelf) {
117             $bookshelf->appendBook($book);
118             Activity::add(ActivityType::BOOKSHELF_UPDATE, $bookshelf);
119         }
120
121         return redirect($book->getUrl());
122     }
123
124     /**
125      * Display the specified book.
126      */
127     public function show(Request $request, ActivityQueries $activities, string $slug)
128     {
129         $book = $this->queries->findVisibleBySlugOrFail($slug);
130         $bookChildren = (new BookContents($book))->getTree(true);
131         $bookParentShelves = $book->shelves()->scopes('visible')->get();
132
133         View::incrementFor($book);
134         if ($request->has('shelf')) {
135             $this->shelfContext->setShelfContext(intval($request->get('shelf')));
136         }
137
138         $this->setPageTitle($book->getShortName());
139
140         return view('books.show', [
141             'book'              => $book,
142             'current'           => $book,
143             'bookChildren'      => $bookChildren,
144             'bookParentShelves' => $bookParentShelves,
145             'watchOptions'      => new UserEntityWatchOptions(user(), $book),
146             'activity'          => $activities->entityActivity($book, 20, 1),
147             'referenceCount'    => $this->referenceFetcher->getReferenceCountToEntity($book),
148         ]);
149     }
150
151     /**
152      * Show the form for editing the specified book.
153      */
154     public function edit(string $slug)
155     {
156         $book = $this->queries->findVisibleBySlugOrFail($slug);
157         $this->checkOwnablePermission('book-update', $book);
158         $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
159
160         return view('books.edit', ['book' => $book, 'current' => $book]);
161     }
162
163     /**
164      * Update the specified book in storage.
165      *
166      * @throws ImageUploadException
167      * @throws ValidationException
168      * @throws Throwable
169      */
170     public function update(Request $request, string $slug)
171     {
172         $book = $this->queries->findVisibleBySlugOrFail($slug);
173         $this->checkOwnablePermission('book-update', $book);
174
175         $validated = $this->validate($request, [
176             'name'                => ['required', 'string', 'max:255'],
177             'description_html'    => ['string', 'max:2000'],
178             'image'               => array_merge(['nullable'], $this->getImageValidationRules()),
179             'tags'                => ['array'],
180             'default_template_id' => ['nullable', 'integer'],
181         ]);
182
183         if ($request->has('image_reset')) {
184             $validated['image'] = null;
185         } elseif (array_key_exists('image', $validated) && is_null($validated['image'])) {
186             unset($validated['image']);
187         }
188
189         $book = $this->bookRepo->update($book, $validated);
190
191         return redirect($book->getUrl());
192     }
193
194     /**
195      * Shows the page to confirm deletion.
196      */
197     public function showDelete(string $bookSlug)
198     {
199         $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
200         $this->checkOwnablePermission('book-delete', $book);
201         $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
202
203         return view('books.delete', ['book' => $book, 'current' => $book]);
204     }
205
206     /**
207      * Remove the specified book from the system.
208      *
209      * @throws Throwable
210      */
211     public function destroy(string $bookSlug)
212     {
213         $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
214         $this->checkOwnablePermission('book-delete', $book);
215
216         $this->bookRepo->destroy($book);
217
218         return redirect('/books');
219     }
220
221     /**
222      * Show the view to copy a book.
223      *
224      * @throws NotFoundException
225      */
226     public function showCopy(string $bookSlug)
227     {
228         $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
229         $this->checkOwnablePermission('book-view', $book);
230
231         session()->flashInput(['name' => $book->name]);
232
233         return view('books.copy', [
234             'book' => $book,
235         ]);
236     }
237
238     /**
239      * Create a copy of a book within the requested target destination.
240      *
241      * @throws NotFoundException
242      */
243     public function copy(Request $request, Cloner $cloner, string $bookSlug)
244     {
245         $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
246         $this->checkOwnablePermission('book-view', $book);
247         $this->checkPermission('book-create-all');
248
249         $newName = $request->get('name') ?: $book->name;
250         $bookCopy = $cloner->cloneBook($book, $newName);
251         $this->showSuccessNotification(trans('entities.books_copy_success'));
252
253         return redirect($bookCopy->getUrl());
254     }
255
256     /**
257      * Convert the chapter to a book.
258      */
259     public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
260     {
261         $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
262         $this->checkOwnablePermission('book-update', $book);
263         $this->checkOwnablePermission('book-delete', $book);
264         $this->checkPermission('bookshelf-create-all');
265         $this->checkPermission('book-create-all');
266
267         $shelf = (new DatabaseTransaction(function () use ($book, $transformer) {
268             return $transformer->transformBookToShelf($book);
269         }))->run();
270
271         return redirect($shelf->getUrl());
272     }
273 }