]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/PageController.php
4818b52116ce968f9533fe712400565a8f96d6d8
[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         // Check for active editing or time conflict
262         $warnings = [];
263         $jsonResponseWarning = '';
264         $editActivity = new PageEditActivity($page);
265         if ($editActivity->hasActiveEditing()) {
266             $warnings[] = $editActivity->activeEditingMessage();
267         }
268         $userDraft = $this->pageRepo->getUserDraft($page);
269         if ($userDraft !== null) {
270             if ($editActivity->hasPageBeenUpdatedSinceDraftSaved($userDraft)) {
271                 $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
272             }
273         }
274         if (count($warnings) > 0) {
275             $jsonResponseWarning = implode("\n", $warnings);
276         }
277
278         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
279
280         $updateTime = $draft->updated_at->timestamp;
281
282         return response()->json([
283             'status'    => 'success',
284             'message'   => trans('entities.pages_edit_draft_save_at'),
285             'warning'   => $jsonResponseWarning,
286             'timestamp' => $updateTime,
287         ]);
288     }
289
290     /**
291      * Redirect from a special link url which uses the page id rather than the name.
292      *
293      * @throws NotFoundException
294      */
295     public function redirectFromLink(int $pageId)
296     {
297         $page = $this->pageRepo->getById($pageId);
298
299         return redirect($page->getUrl());
300     }
301
302     /**
303      * Show the deletion page for the specified page.
304      *
305      * @throws NotFoundException
306      */
307     public function showDelete(string $bookSlug, string $pageSlug)
308     {
309         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
310         $this->checkOwnablePermission('page-delete', $page);
311         $this->setPageTitle(trans('entities.pages_delete_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      * Show the deletion page for the specified page.
322      *
323      * @throws NotFoundException
324      */
325     public function showDeleteDraft(string $bookSlug, int $pageId)
326     {
327         $page = $this->pageRepo->getById($pageId);
328         $this->checkOwnablePermission('page-update', $page);
329         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
330
331         return view('pages.delete', [
332             'book'    => $page->book,
333             'page'    => $page,
334             'current' => $page,
335         ]);
336     }
337
338     /**
339      * Remove the specified page from storage.
340      *
341      * @throws NotFoundException
342      * @throws Throwable
343      */
344     public function destroy(string $bookSlug, string $pageSlug)
345     {
346         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
347         $this->checkOwnablePermission('page-delete', $page);
348         $parent = $page->getParent();
349
350         $this->pageRepo->destroy($page);
351
352         return redirect($parent->getUrl());
353     }
354
355     /**
356      * Remove the specified draft page from storage.
357      *
358      * @throws NotFoundException
359      * @throws Throwable
360      */
361     public function destroyDraft(string $bookSlug, int $pageId)
362     {
363         $page = $this->pageRepo->getById($pageId);
364         $book = $page->book;
365         $chapter = $page->chapter;
366         $this->checkOwnablePermission('page-update', $page);
367
368         $this->pageRepo->destroy($page);
369
370         $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
371
372         if ($chapter && userCan('view', $chapter)) {
373             return redirect($chapter->getUrl());
374         }
375
376         return redirect($book->getUrl());
377     }
378
379     /**
380      * Show a listing of recently created pages.
381      */
382     public function showRecentlyUpdated()
383     {
384         $pages = Page::visible()->orderBy('updated_at', 'desc')
385             ->paginate(20)
386             ->setPath(url('/pages/recently-updated'));
387
388         return view('common.detailed-listing-paginated', [
389             'title'    => trans('entities.recently_updated_pages'),
390             'entities' => $pages,
391         ]);
392     }
393
394     /**
395      * Show the view to choose a new parent to move a page into.
396      *
397      * @throws NotFoundException
398      */
399     public function showMove(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         return view('pages.move', [
406             'book' => $page->book,
407             'page' => $page,
408         ]);
409     }
410
411     /**
412      * Does the action of moving the location of a page.
413      *
414      * @throws NotFoundException
415      * @throws Throwable
416      */
417     public function move(Request $request, string $bookSlug, string $pageSlug)
418     {
419         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
420         $this->checkOwnablePermission('page-update', $page);
421         $this->checkOwnablePermission('page-delete', $page);
422
423         $entitySelection = $request->get('entity_selection', null);
424         if ($entitySelection === null || $entitySelection === '') {
425             return redirect($page->getUrl());
426         }
427
428         try {
429             $parent = $this->pageRepo->move($page, $entitySelection);
430         } catch (Exception $exception) {
431             if ($exception instanceof PermissionsException) {
432                 $this->showPermissionError();
433             }
434
435             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
436
437             return redirect()->back();
438         }
439
440         $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
441
442         return redirect($page->getUrl());
443     }
444
445     /**
446      * Show the view to copy a page.
447      *
448      * @throws NotFoundException
449      */
450     public function showCopy(string $bookSlug, string $pageSlug)
451     {
452         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
453         $this->checkOwnablePermission('page-view', $page);
454         session()->flashInput(['name' => $page->name]);
455
456         return view('pages.copy', [
457             'book' => $page->book,
458             'page' => $page,
459         ]);
460     }
461
462     /**
463      * Create a copy of a page within the requested target destination.
464      *
465      * @throws NotFoundException
466      * @throws Throwable
467      */
468     public function copy(Request $request, string $bookSlug, string $pageSlug)
469     {
470         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
471         $this->checkOwnablePermission('page-view', $page);
472
473         $entitySelection = $request->get('entity_selection', null) ?? null;
474         $newName = $request->get('name', null);
475
476         try {
477             $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
478         } catch (Exception $exception) {
479             if ($exception instanceof PermissionsException) {
480                 $this->showPermissionError();
481             }
482
483             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
484
485             return redirect()->back();
486         }
487
488         $this->showSuccessNotification(trans('entities.pages_copy_success'));
489
490         return redirect($pageCopy->getUrl());
491     }
492
493     /**
494      * Show the Permissions view.
495      *
496      * @throws NotFoundException
497      */
498     public function showPermissions(string $bookSlug, string $pageSlug)
499     {
500         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
501         $this->checkOwnablePermission('restrictions-manage', $page);
502
503         return view('pages.permissions', [
504             'page' => $page,
505         ]);
506     }
507
508     /**
509      * Set the permissions for this page.
510      *
511      * @throws NotFoundException
512      * @throws Throwable
513      */
514     public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $pageSlug)
515     {
516         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
517         $this->checkOwnablePermission('restrictions-manage', $page);
518
519         $permissionsUpdater->updateFromPermissionsForm($page, $request);
520
521         $this->showSuccessNotification(trans('entities.pages_permissions_success'));
522
523         return redirect($page->getUrl());
524     }
525 }