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