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