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