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