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