]> BookStack Code Mirror - bookstack/blob - app/Entities/Controllers/PageController.php
added template to chapter API controller
[bookstack] / app / Entities / Controllers / PageController.php
1 <?php
2
3 namespace BookStack\Entities\Controllers;
4
5 use BookStack\Activity\Models\View;
6 use BookStack\Activity\Tools\CommentTree;
7 use BookStack\Activity\Tools\UserEntityWatchOptions;
8 use BookStack\Entities\Models\Book;
9 use BookStack\Entities\Models\Chapter;
10 use BookStack\Entities\Models\Page;
11 use BookStack\Entities\Repos\PageRepo;
12 use BookStack\Entities\Tools\BookContents;
13 use BookStack\Entities\Tools\Cloner;
14 use BookStack\Entities\Tools\NextPreviousContentLocator;
15 use BookStack\Entities\Tools\PageContent;
16 use BookStack\Entities\Tools\PageEditActivity;
17 use BookStack\Entities\Tools\PageEditorData;
18 use BookStack\Exceptions\NotFoundException;
19 use BookStack\Exceptions\PermissionsException;
20 use BookStack\Http\Controller;
21 use BookStack\References\ReferenceFetcher;
22 use Exception;
23 use Illuminate\Database\Eloquent\Relations\BelongsTo;
24 use Illuminate\Http\Request;
25 use Illuminate\Validation\ValidationException;
26 use Throwable;
27
28 class PageController extends Controller
29 {
30     public function __construct(
31         protected PageRepo $pageRepo,
32         protected ReferenceFetcher $referenceFetcher
33     ) {
34     }
35
36     /**
37      * Show the form for creating a new page.
38      *
39      * @throws Throwable
40      */
41     public function create(string $bookSlug, string $chapterSlug = null)
42     {
43         $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
44         $this->checkOwnablePermission('page-create', $parent);
45
46         // Redirect to draft edit screen if signed in
47         if ($this->isSignedIn()) {
48             $draft = $this->pageRepo->getNewDraftPage($parent);
49
50             return redirect($draft->getUrl());
51         }
52
53         // Otherwise show the edit view if they're a guest
54         $this->setPageTitle(trans('entities.pages_new'));
55
56         return view('pages.guest-create', ['parent' => $parent]);
57     }
58
59     /**
60      * Create a new page as a guest user.
61      *
62      * @throws ValidationException
63      */
64     public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
65     {
66         $this->validate($request, [
67             'name' => ['required', 'string', 'max:255'],
68         ]);
69
70         $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
71         $this->checkOwnablePermission('page-create', $parent);
72
73         $page = $this->pageRepo->getNewDraftPage($parent);
74         $this->pageRepo->publishDraft($page, [
75             'name' => $request->get('name'),
76         ]);
77
78         return redirect($page->getUrl('/edit'));
79     }
80
81     /**
82      * Show form to continue editing a draft page.
83      *
84      * @throws NotFoundException
85      */
86     public function editDraft(Request $request, string $bookSlug, int $pageId)
87     {
88         $draft = $this->pageRepo->getById($pageId);
89         $this->checkOwnablePermission('page-create', $draft->getParent());
90
91         $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
92         $this->setPageTitle(trans('entities.pages_edit_draft'));
93
94         return view('pages.edit', $editorData->getViewData());
95     }
96
97     /**
98      * Store a new page by changing a draft into a page.
99      *
100      * @throws NotFoundException
101      * @throws ValidationException
102      */
103     public function store(Request $request, string $bookSlug, int $pageId)
104     {
105         $this->validate($request, [
106             'name' => ['required', 'string', 'max:255'],
107         ]);
108         $draftPage = $this->pageRepo->getById($pageId);
109         $this->checkOwnablePermission('page-create', $draftPage->getParent());
110
111         $page = $this->pageRepo->publishDraft($draftPage, $request->all());
112
113         return redirect($page->getUrl());
114     }
115
116     /**
117      * Display the specified page.
118      * If the page is not found via the slug the revisions are searched for a match.
119      *
120      * @throws NotFoundException
121      */
122     public function show(string $bookSlug, string $pageSlug)
123     {
124         try {
125             $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
126         } catch (NotFoundException $e) {
127             $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
128
129             if ($page === null) {
130                 throw $e;
131             }
132
133             return redirect($page->getUrl());
134         }
135
136         $this->checkOwnablePermission('page-view', $page);
137
138         $pageContent = (new PageContent($page));
139         $page->html = $pageContent->render();
140         $pageNav = $pageContent->getNavigation($page->html);
141
142         $sidebarTree = (new BookContents($page->book))->getTree();
143         $commentTree = (new CommentTree($page));
144         $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
145
146         View::incrementFor($page);
147         $this->setPageTitle($page->getShortName());
148
149         return view('pages.show', [
150             'page'            => $page,
151             'book'            => $page->book,
152             'current'         => $page,
153             'sidebarTree'     => $sidebarTree,
154             'commentTree'     => $commentTree,
155             'pageNav'         => $pageNav,
156             'watchOptions'    => new UserEntityWatchOptions(user(), $page),
157             'next'            => $nextPreviousLocator->getNext(),
158             'previous'        => $nextPreviousLocator->getPrevious(),
159             'referenceCount'  => $this->referenceFetcher->getReferenceCountToEntity($page),
160         ]);
161     }
162
163     /**
164      * Get page from an ajax request.
165      *
166      * @throws NotFoundException
167      */
168     public function getPageAjax(int $pageId)
169     {
170         $page = $this->pageRepo->getById($pageId);
171         $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
172         $page->makeHidden(['book']);
173
174         return response()->json($page);
175     }
176
177     /**
178      * Show the form for editing the specified page.
179      *
180      * @throws NotFoundException
181      */
182     public function edit(Request $request, string $bookSlug, string $pageSlug)
183     {
184         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
185         $this->checkOwnablePermission('page-update', $page);
186
187         $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
188         if ($editorData->getWarnings()) {
189             $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
190         }
191
192         $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
193
194         return view('pages.edit', $editorData->getViewData());
195     }
196
197     /**
198      * Update the specified page in storage.
199      *
200      * @throws ValidationException
201      * @throws NotFoundException
202      */
203     public function update(Request $request, string $bookSlug, string $pageSlug)
204     {
205         $this->validate($request, [
206             'name' => ['required', 'string', 'max:255'],
207         ]);
208         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
209         $this->checkOwnablePermission('page-update', $page);
210
211         $this->pageRepo->update($page, $request->all());
212
213         return redirect($page->getUrl());
214     }
215
216     /**
217      * Save a draft update as a revision.
218      *
219      * @throws NotFoundException
220      */
221     public function saveDraft(Request $request, int $pageId)
222     {
223         $page = $this->pageRepo->getById($pageId);
224         $this->checkOwnablePermission('page-update', $page);
225
226         if (!$this->isSignedIn()) {
227             return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
228         }
229
230         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
231         $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
232
233         return response()->json([
234             'status'    => 'success',
235             'message'   => trans('entities.pages_edit_draft_save_at'),
236             'warning'   => implode("\n", $warnings),
237             'timestamp' => $draft->updated_at->timestamp,
238         ]);
239     }
240
241     /**
242      * Redirect from a special link url which uses the page id rather than the name.
243      *
244      * @throws NotFoundException
245      */
246     public function redirectFromLink(int $pageId)
247     {
248         $page = $this->pageRepo->getById($pageId);
249
250         return redirect($page->getUrl());
251     }
252
253     /**
254      * Show the deletion page for the specified page.
255      *
256      * @throws NotFoundException
257      */
258     public function showDelete(string $bookSlug, string $pageSlug)
259     {
260         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
261         $this->checkOwnablePermission('page-delete', $page);
262         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
263         $usedAsTemplate =
264             Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
265             Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
266
267         return view('pages.delete', [
268             'book'    => $page->book,
269             'page'    => $page,
270             'current' => $page,
271             'usedAsTemplate' => $usedAsTemplate,
272         ]);
273     }
274
275     /**
276      * Show the deletion page for the specified page.
277      *
278      * @throws NotFoundException
279      */
280     public function showDeleteDraft(string $bookSlug, int $pageId)
281     {
282         $page = $this->pageRepo->getById($pageId);
283         $this->checkOwnablePermission('page-update', $page);
284         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
285         $usedAsTemplate =
286             Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
287             Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
288
289         return view('pages.delete', [
290             'book'    => $page->book,
291             'page'    => $page,
292             'current' => $page,
293             'usedAsTemplate' => $usedAsTemplate,
294         ]);
295     }
296
297     /**
298      * Remove the specified page from storage.
299      *
300      * @throws NotFoundException
301      * @throws Throwable
302      */
303     public function destroy(string $bookSlug, string $pageSlug)
304     {
305         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
306         $this->checkOwnablePermission('page-delete', $page);
307         $parent = $page->getParent();
308
309         $this->pageRepo->destroy($page);
310
311         return redirect($parent->getUrl());
312     }
313
314     /**
315      * Remove the specified draft page from storage.
316      *
317      * @throws NotFoundException
318      * @throws Throwable
319      */
320     public function destroyDraft(string $bookSlug, int $pageId)
321     {
322         $page = $this->pageRepo->getById($pageId);
323         $book = $page->book;
324         $chapter = $page->chapter;
325         $this->checkOwnablePermission('page-update', $page);
326
327         $this->pageRepo->destroy($page);
328
329         $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
330
331         if ($chapter && userCan('view', $chapter)) {
332             return redirect($chapter->getUrl());
333         }
334
335         return redirect($book->getUrl());
336     }
337
338     /**
339      * Show a listing of recently created pages.
340      */
341     public function showRecentlyUpdated()
342     {
343         $visibleBelongsScope = function (BelongsTo $query) {
344             $query->scopes('visible');
345         };
346
347         $pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
348             ->orderBy('updated_at', 'desc')
349             ->paginate(20)
350             ->setPath(url('/pages/recently-updated'));
351
352         $this->setPageTitle(trans('entities.recently_updated_pages'));
353
354         return view('common.detailed-listing-paginated', [
355             'title'         => trans('entities.recently_updated_pages'),
356             'entities'      => $pages,
357             'showUpdatedBy' => true,
358             'showPath'      => true,
359         ]);
360     }
361
362     /**
363      * Show the view to choose a new parent to move a page into.
364      *
365      * @throws NotFoundException
366      */
367     public function showMove(string $bookSlug, string $pageSlug)
368     {
369         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
370         $this->checkOwnablePermission('page-update', $page);
371         $this->checkOwnablePermission('page-delete', $page);
372
373         return view('pages.move', [
374             'book' => $page->book,
375             'page' => $page,
376         ]);
377     }
378
379     /**
380      * Does the action of moving the location of a page.
381      *
382      * @throws NotFoundException
383      * @throws Throwable
384      */
385     public function move(Request $request, string $bookSlug, string $pageSlug)
386     {
387         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
388         $this->checkOwnablePermission('page-update', $page);
389         $this->checkOwnablePermission('page-delete', $page);
390
391         $entitySelection = $request->get('entity_selection', null);
392         if ($entitySelection === null || $entitySelection === '') {
393             return redirect($page->getUrl());
394         }
395
396         try {
397             $this->pageRepo->move($page, $entitySelection);
398         } catch (PermissionsException $exception) {
399             $this->showPermissionError();
400         } catch (Exception $exception) {
401             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
402
403             return redirect($page->getUrl('/move'));
404         }
405
406         return redirect($page->getUrl());
407     }
408
409     /**
410      * Show the view to copy a page.
411      *
412      * @throws NotFoundException
413      */
414     public function showCopy(string $bookSlug, string $pageSlug)
415     {
416         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
417         $this->checkOwnablePermission('page-view', $page);
418         session()->flashInput(['name' => $page->name]);
419
420         return view('pages.copy', [
421             'book' => $page->book,
422             'page' => $page,
423         ]);
424     }
425
426     /**
427      * Create a copy of a page within the requested target destination.
428      *
429      * @throws NotFoundException
430      * @throws Throwable
431      */
432     public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
433     {
434         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
435         $this->checkOwnablePermission('page-view', $page);
436
437         $entitySelection = $request->get('entity_selection') ?: null;
438         $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
439
440         if (is_null($newParent)) {
441             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
442
443             return redirect($page->getUrl('/copy'));
444         }
445
446         $this->checkOwnablePermission('page-create', $newParent);
447
448         $newName = $request->get('name') ?: $page->name;
449         $pageCopy = $cloner->clonePage($page, $newParent, $newName);
450         $this->showSuccessNotification(trans('entities.pages_copy_success'));
451
452         return redirect($pageCopy->getUrl());
453     }
454 }