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