]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/PageController.php
Guest create page: name field autofocus
[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\Book;
7 use BookStack\Entities\Models\Page;
8 use BookStack\Entities\Repos\PageRepo;
9 use BookStack\Entities\Tools\BookContents;
10 use BookStack\Entities\Tools\Cloner;
11 use BookStack\Entities\Tools\NextPreviousContentLocator;
12 use BookStack\Entities\Tools\PageContent;
13 use BookStack\Entities\Tools\PageEditActivity;
14 use BookStack\Entities\Tools\PageEditorData;
15 use BookStack\Exceptions\NotFoundException;
16 use BookStack\Exceptions\PermissionsException;
17 use BookStack\References\ReferenceFetcher;
18 use Exception;
19 use Illuminate\Database\Eloquent\Relations\BelongsTo;
20 use Illuminate\Http\Request;
21 use Illuminate\Validation\ValidationException;
22 use Throwable;
23
24 class PageController extends Controller
25 {
26     protected PageRepo $pageRepo;
27     protected ReferenceFetcher $referenceFetcher;
28
29     /**
30      * PageController constructor.
31      */
32     public function __construct(PageRepo $pageRepo, ReferenceFetcher $referenceFetcher)
33     {
34         $this->pageRepo = $pageRepo;
35         $this->referenceFetcher = $referenceFetcher;
36     }
37
38     /**
39      * Show the form for creating a new page.
40      *
41      * @throws Throwable
42      */
43     public function create(string $bookSlug, string $chapterSlug = null)
44     {
45         $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
46         $this->checkOwnablePermission('page-create', $parent);
47
48         // Redirect to draft edit screen if signed in
49         if ($this->isSignedIn()) {
50             $draft = $this->pageRepo->getNewDraftPage($parent);
51
52             return redirect($draft->getUrl());
53         }
54
55         // Otherwise show the edit view if they're a guest
56         $this->setPageTitle(trans('entities.pages_new'));
57
58         return view('pages.guest-create', ['parent' => $parent]);
59     }
60
61     /**
62      * Create a new page as a guest user.
63      *
64      * @throws ValidationException
65      */
66     public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
67     {
68         $this->validate($request, [
69             'name' => ['required', 'string', 'max:255'],
70         ]);
71
72         $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
73         $this->checkOwnablePermission('page-create', $parent);
74
75         $page = $this->pageRepo->getNewDraftPage($parent);
76         $this->pageRepo->publishDraft($page, [
77             'name' => $request->get('name'),
78         ]);
79
80         return redirect($page->getUrl('/edit'));
81     }
82
83     /**
84      * Show form to continue editing a draft page.
85      *
86      * @throws NotFoundException
87      */
88     public function editDraft(Request $request, string $bookSlug, int $pageId)
89     {
90         $draft = $this->pageRepo->getById($pageId);
91         $this->checkOwnablePermission('page-create', $draft->getParent());
92
93         $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
94         $this->setPageTitle(trans('entities.pages_edit_draft'));
95
96         return view('pages.edit', $editorData->getViewData());
97     }
98
99     /**
100      * Store a new page by changing a draft into a page.
101      *
102      * @throws NotFoundException
103      * @throws ValidationException
104      */
105     public function store(Request $request, string $bookSlug, int $pageId)
106     {
107         $this->validate($request, [
108             'name' => ['required', 'string', 'max:255'],
109         ]);
110         $draftPage = $this->pageRepo->getById($pageId);
111         $this->checkOwnablePermission('page-create', $draftPage->getParent());
112
113         $page = $this->pageRepo->publishDraft($draftPage, $request->all());
114
115         return redirect($page->getUrl());
116     }
117
118     /**
119      * Display the specified page.
120      * If the page is not found via the slug the revisions are searched for a match.
121      *
122      * @throws NotFoundException
123      */
124     public function show(string $bookSlug, string $pageSlug)
125     {
126         try {
127             $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
128         } catch (NotFoundException $e) {
129             $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
130
131             if ($page === null) {
132                 throw $e;
133             }
134
135             return redirect($page->getUrl());
136         }
137
138         $this->checkOwnablePermission('page-view', $page);
139
140         $pageContent = (new PageContent($page));
141         $page->html = $pageContent->render();
142         $sidebarTree = (new BookContents($page->book))->getTree();
143         $pageNav = $pageContent->getNavigation($page->html);
144
145         // Check if page comments are enabled
146         $commentsEnabled = !setting('app-disable-comments');
147         if ($commentsEnabled) {
148             $page->load(['comments.createdBy']);
149         }
150
151         $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
152
153         View::incrementFor($page);
154         $this->setPageTitle($page->getShortName());
155
156         return view('pages.show', [
157             'page'            => $page,
158             'book'            => $page->book,
159             'current'         => $page,
160             'sidebarTree'     => $sidebarTree,
161             'commentsEnabled' => $commentsEnabled,
162             'pageNav'         => $pageNav,
163             'next'            => $nextPreviousLocator->getNext(),
164             'previous'        => $nextPreviousLocator->getPrevious(),
165             'referenceCount'  => $this->referenceFetcher->getPageReferenceCountToEntity($page),
166         ]);
167     }
168
169     /**
170      * Get page from an ajax request.
171      *
172      * @throws NotFoundException
173      */
174     public function getPageAjax(int $pageId)
175     {
176         $page = $this->pageRepo->getById($pageId);
177         $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
178         $page->makeHidden(['book']);
179
180         return response()->json($page);
181     }
182
183     /**
184      * Show the form for editing the specified page.
185      *
186      * @throws NotFoundException
187      */
188     public function edit(Request $request, string $bookSlug, string $pageSlug)
189     {
190         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
191         $this->checkOwnablePermission('page-update', $page);
192
193         $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
194         if ($editorData->getWarnings()) {
195             $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
196         }
197
198         $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
199
200         return view('pages.edit', $editorData->getViewData());
201     }
202
203     /**
204      * Update the specified page in storage.
205      *
206      * @throws ValidationException
207      * @throws NotFoundException
208      */
209     public function update(Request $request, string $bookSlug, string $pageSlug)
210     {
211         $this->validate($request, [
212             'name' => ['required', 'string', 'max:255'],
213         ]);
214         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
215         $this->checkOwnablePermission('page-update', $page);
216
217         $this->pageRepo->update($page, $request->all());
218
219         return redirect($page->getUrl());
220     }
221
222     /**
223      * Save a draft update as a revision.
224      *
225      * @throws NotFoundException
226      */
227     public function saveDraft(Request $request, int $pageId)
228     {
229         $page = $this->pageRepo->getById($pageId);
230         $this->checkOwnablePermission('page-update', $page);
231
232         if (!$this->isSignedIn()) {
233             return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
234         }
235
236         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
237         $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
238
239         return response()->json([
240             'status'    => 'success',
241             'message'   => trans('entities.pages_edit_draft_save_at'),
242             'warning'   => implode("\n", $warnings),
243             'timestamp' => $draft->updated_at->timestamp,
244         ]);
245     }
246
247     /**
248      * Redirect from a special link url which uses the page id rather than the name.
249      *
250      * @throws NotFoundException
251      */
252     public function redirectFromLink(int $pageId)
253     {
254         $page = $this->pageRepo->getById($pageId);
255
256         return redirect($page->getUrl());
257     }
258
259     /**
260      * Show the deletion page for the specified page.
261      *
262      * @throws NotFoundException
263      */
264     public function showDelete(string $bookSlug, string $pageSlug)
265     {
266         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
267         $this->checkOwnablePermission('page-delete', $page);
268         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
269         $times_used_as_template = Book::where('default_template', '=', $page->id)->count();
270
271         return view('pages.delete', [
272             'book'    => $page->book,
273             'page'    => $page,
274             'current' => $page,
275             'times_used_as_template' => $times_used_as_template,
276         ]);
277     }
278
279     /**
280      * Show the deletion page for the specified page.
281      *
282      * @throws NotFoundException
283      */
284     public function showDeleteDraft(string $bookSlug, int $pageId)
285     {
286         $page = $this->pageRepo->getById($pageId);
287         $this->checkOwnablePermission('page-update', $page);
288         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
289
290         return view('pages.delete', [
291             'book'    => $page->book,
292             'page'    => $page,
293             'current' => $page,
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             $parent = $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()->back();
404         }
405
406         $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
407
408         return redirect($page->getUrl());
409     }
410
411     /**
412      * Show the view to copy a page.
413      *
414      * @throws NotFoundException
415      */
416     public function showCopy(string $bookSlug, string $pageSlug)
417     {
418         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
419         $this->checkOwnablePermission('page-view', $page);
420         session()->flashInput(['name' => $page->name]);
421
422         return view('pages.copy', [
423             'book' => $page->book,
424             'page' => $page,
425         ]);
426     }
427
428     /**
429      * Create a copy of a page within the requested target destination.
430      *
431      * @throws NotFoundException
432      * @throws Throwable
433      */
434     public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
435     {
436         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
437         $this->checkOwnablePermission('page-view', $page);
438
439         $entitySelection = $request->get('entity_selection') ?: null;
440         $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
441
442         if (is_null($newParent)) {
443             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
444
445             return redirect()->back();
446         }
447
448         $this->checkOwnablePermission('page-create', $newParent);
449
450         $newName = $request->get('name') ?: $page->name;
451         $pageCopy = $cloner->clonePage($page, $newParent, $newName);
452         $this->showSuccessNotification(trans('entities.pages_copy_success'));
453
454         return redirect($pageCopy->getUrl());
455     }
456 }