]> BookStack Code Mirror - bookstack/blob - app/Entities/Controllers/PageController.php
Played around with a new app structure
[bookstack] / app / Entities / Controllers / PageController.php
1 <?php
2
3 namespace BookStack\Entities\Controllers;
4
5 use BookStack\Activity\Models\View;
6 use BookStack\Entities\Models\Page;
7 use BookStack\Entities\Repos\PageRepo;
8 use BookStack\Entities\Tools\BookContents;
9 use BookStack\Entities\Tools\Cloner;
10 use BookStack\Entities\Tools\NextPreviousContentLocator;
11 use BookStack\Entities\Tools\PageContent;
12 use BookStack\Entities\Tools\PageEditActivity;
13 use BookStack\Entities\Tools\PageEditorData;
14 use BookStack\Exceptions\NotFoundException;
15 use BookStack\Exceptions\PermissionsException;
16 use BookStack\Http\Controllers\Controller;
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             'html' => '',
79         ]);
80
81         return redirect($page->getUrl('/edit'));
82     }
83
84     /**
85      * Show form to continue editing a draft page.
86      *
87      * @throws NotFoundException
88      */
89     public function editDraft(Request $request, string $bookSlug, int $pageId)
90     {
91         $draft = $this->pageRepo->getById($pageId);
92         $this->checkOwnablePermission('page-create', $draft->getParent());
93
94         $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
95         $this->setPageTitle(trans('entities.pages_edit_draft'));
96
97         return view('pages.edit', $editorData->getViewData());
98     }
99
100     /**
101      * Store a new page by changing a draft into a page.
102      *
103      * @throws NotFoundException
104      * @throws ValidationException
105      */
106     public function store(Request $request, string $bookSlug, int $pageId)
107     {
108         $this->validate($request, [
109             'name' => ['required', 'string', 'max:255'],
110         ]);
111         $draftPage = $this->pageRepo->getById($pageId);
112         $this->checkOwnablePermission('page-create', $draftPage->getParent());
113
114         $page = $this->pageRepo->publishDraft($draftPage, $request->all());
115
116         return redirect($page->getUrl());
117     }
118
119     /**
120      * Display the specified page.
121      * If the page is not found via the slug the revisions are searched for a match.
122      *
123      * @throws NotFoundException
124      */
125     public function show(string $bookSlug, string $pageSlug)
126     {
127         try {
128             $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
129         } catch (NotFoundException $e) {
130             $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
131
132             if ($page === null) {
133                 throw $e;
134             }
135
136             return redirect($page->getUrl());
137         }
138
139         $this->checkOwnablePermission('page-view', $page);
140
141         $pageContent = (new PageContent($page));
142         $page->html = $pageContent->render();
143         $sidebarTree = (new BookContents($page->book))->getTree();
144         $pageNav = $pageContent->getNavigation($page->html);
145
146         // Check if page comments are enabled
147         $commentsEnabled = !setting('app-disable-comments');
148         if ($commentsEnabled) {
149             $page->load(['comments.createdBy']);
150         }
151
152         $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
153
154         View::incrementFor($page);
155         $this->setPageTitle($page->getShortName());
156
157         return view('pages.show', [
158             'page'            => $page,
159             'book'            => $page->book,
160             'current'         => $page,
161             'sidebarTree'     => $sidebarTree,
162             'commentsEnabled' => $commentsEnabled,
163             'pageNav'         => $pageNav,
164             'next'            => $nextPreviousLocator->getNext(),
165             'previous'        => $nextPreviousLocator->getPrevious(),
166             'referenceCount'  => $this->referenceFetcher->getPageReferenceCountToEntity($page),
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->makeHidden(['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(Request $request, string $bookSlug, string $pageSlug)
190     {
191         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
192         $this->checkOwnablePermission('page-update', $page);
193
194         $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
195         if ($editorData->getWarnings()) {
196             $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
197         }
198
199         $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
200
201         return view('pages.edit', $editorData->getViewData());
202     }
203
204     /**
205      * Update the specified page in storage.
206      *
207      * @throws ValidationException
208      * @throws NotFoundException
209      */
210     public function update(Request $request, string $bookSlug, string $pageSlug)
211     {
212         $this->validate($request, [
213             'name' => ['required', 'string', 'max:255'],
214         ]);
215         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
216         $this->checkOwnablePermission('page-update', $page);
217
218         $this->pageRepo->update($page, $request->all());
219
220         return redirect($page->getUrl());
221     }
222
223     /**
224      * Save a draft update as a revision.
225      *
226      * @throws NotFoundException
227      */
228     public function saveDraft(Request $request, int $pageId)
229     {
230         $page = $this->pageRepo->getById($pageId);
231         $this->checkOwnablePermission('page-update', $page);
232
233         if (!$this->isSignedIn()) {
234             return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
235         }
236
237         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
238         $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
239
240         return response()->json([
241             'status'    => 'success',
242             'message'   => trans('entities.pages_edit_draft_save_at'),
243             'warning'   => implode("\n", $warnings),
244             'timestamp' => $draft->updated_at->timestamp,
245         ]);
246     }
247
248     /**
249      * Redirect from a special link url which uses the page id rather than the name.
250      *
251      * @throws NotFoundException
252      */
253     public function redirectFromLink(int $pageId)
254     {
255         $page = $this->pageRepo->getById($pageId);
256
257         return redirect($page->getUrl());
258     }
259
260     /**
261      * Show the deletion page for the specified page.
262      *
263      * @throws NotFoundException
264      */
265     public function showDelete(string $bookSlug, string $pageSlug)
266     {
267         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
268         $this->checkOwnablePermission('page-delete', $page);
269         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
270
271         return view('pages.delete', [
272             'book'    => $page->book,
273             'page'    => $page,
274             'current' => $page,
275         ]);
276     }
277
278     /**
279      * Show the deletion page for the specified page.
280      *
281      * @throws NotFoundException
282      */
283     public function showDeleteDraft(string $bookSlug, int $pageId)
284     {
285         $page = $this->pageRepo->getById($pageId);
286         $this->checkOwnablePermission('page-update', $page);
287         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
288
289         return view('pages.delete', [
290             'book'    => $page->book,
291             'page'    => $page,
292             'current' => $page,
293         ]);
294     }
295
296     /**
297      * Remove the specified page from storage.
298      *
299      * @throws NotFoundException
300      * @throws Throwable
301      */
302     public function destroy(string $bookSlug, string $pageSlug)
303     {
304         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
305         $this->checkOwnablePermission('page-delete', $page);
306         $parent = $page->getParent();
307
308         $this->pageRepo->destroy($page);
309
310         return redirect($parent->getUrl());
311     }
312
313     /**
314      * Remove the specified draft page from storage.
315      *
316      * @throws NotFoundException
317      * @throws Throwable
318      */
319     public function destroyDraft(string $bookSlug, int $pageId)
320     {
321         $page = $this->pageRepo->getById($pageId);
322         $book = $page->book;
323         $chapter = $page->chapter;
324         $this->checkOwnablePermission('page-update', $page);
325
326         $this->pageRepo->destroy($page);
327
328         $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
329
330         if ($chapter && userCan('view', $chapter)) {
331             return redirect($chapter->getUrl());
332         }
333
334         return redirect($book->getUrl());
335     }
336
337     /**
338      * Show a listing of recently created pages.
339      */
340     public function showRecentlyUpdated()
341     {
342         $visibleBelongsScope = function (BelongsTo $query) {
343             $query->scopes('visible');
344         };
345
346         $pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
347             ->orderBy('updated_at', 'desc')
348             ->paginate(20)
349             ->setPath(url('/pages/recently-updated'));
350
351         $this->setPageTitle(trans('entities.recently_updated_pages'));
352
353         return view('common.detailed-listing-paginated', [
354             'title'         => trans('entities.recently_updated_pages'),
355             'entities'      => $pages,
356             'showUpdatedBy' => true,
357             'showPath'      => true,
358         ]);
359     }
360
361     /**
362      * Show the view to choose a new parent to move a page into.
363      *
364      * @throws NotFoundException
365      */
366     public function showMove(string $bookSlug, string $pageSlug)
367     {
368         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
369         $this->checkOwnablePermission('page-update', $page);
370         $this->checkOwnablePermission('page-delete', $page);
371
372         return view('pages.move', [
373             'book' => $page->book,
374             'page' => $page,
375         ]);
376     }
377
378     /**
379      * Does the action of moving the location of a page.
380      *
381      * @throws NotFoundException
382      * @throws Throwable
383      */
384     public function move(Request $request, string $bookSlug, string $pageSlug)
385     {
386         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
387         $this->checkOwnablePermission('page-update', $page);
388         $this->checkOwnablePermission('page-delete', $page);
389
390         $entitySelection = $request->get('entity_selection', null);
391         if ($entitySelection === null || $entitySelection === '') {
392             return redirect($page->getUrl());
393         }
394
395         try {
396             $parent = $this->pageRepo->move($page, $entitySelection);
397         } catch (PermissionsException $exception) {
398             $this->showPermissionError();
399         } catch (Exception $exception) {
400             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
401
402             return redirect()->back();
403         }
404
405         $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
406
407         return redirect($page->getUrl());
408     }
409
410     /**
411      * Show the view to copy a page.
412      *
413      * @throws NotFoundException
414      */
415     public function showCopy(string $bookSlug, string $pageSlug)
416     {
417         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
418         $this->checkOwnablePermission('page-view', $page);
419         session()->flashInput(['name' => $page->name]);
420
421         return view('pages.copy', [
422             'book' => $page->book,
423             'page' => $page,
424         ]);
425     }
426
427     /**
428      * Create a copy of a page within the requested target destination.
429      *
430      * @throws NotFoundException
431      * @throws Throwable
432      */
433     public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
434     {
435         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
436         $this->checkOwnablePermission('page-view', $page);
437
438         $entitySelection = $request->get('entity_selection') ?: null;
439         $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
440
441         if (is_null($newParent)) {
442             $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
443
444             return redirect()->back();
445         }
446
447         $this->checkOwnablePermission('page-create', $newParent);
448
449         $newName = $request->get('name') ?: $page->name;
450         $pageCopy = $cloner->clonePage($page, $newParent, $newName);
451         $this->showSuccessNotification(trans('entities.pages_copy_success'));
452
453         return redirect($pageCopy->getUrl());
454     }
455 }