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