3 namespace BookStack\Entities\Controllers;
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\Repos\PageRepo;
12 use BookStack\Entities\Tools\BookContents;
13 use BookStack\Entities\Tools\Cloner;
14 use BookStack\Entities\Tools\NextPreviousContentLocator;
15 use BookStack\Entities\Tools\PageContent;
16 use BookStack\Entities\Tools\PageEditActivity;
17 use BookStack\Entities\Tools\PageEditorData;
18 use BookStack\Exceptions\NotFoundException;
19 use BookStack\Exceptions\PermissionsException;
20 use BookStack\Http\Controller;
21 use BookStack\References\ReferenceFetcher;
23 use Illuminate\Database\Eloquent\Relations\BelongsTo;
24 use Illuminate\Http\Request;
25 use Illuminate\Validation\ValidationException;
28 class PageController extends Controller
30 public function __construct(
31 protected PageRepo $pageRepo,
32 protected ReferenceFetcher $referenceFetcher
37 * Show the form for creating a new page.
41 public function create(string $bookSlug, string $chapterSlug = null)
43 $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
44 $this->checkOwnablePermission('page-create', $parent);
46 // Redirect to draft edit screen if signed in
47 if ($this->isSignedIn()) {
48 $draft = $this->pageRepo->getNewDraftPage($parent);
50 return redirect($draft->getUrl());
53 // Otherwise show the edit view if they're a guest
54 $this->setPageTitle(trans('entities.pages_new'));
56 return view('pages.guest-create', ['parent' => $parent]);
60 * Create a new page as a guest user.
62 * @throws ValidationException
64 public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
66 $this->validate($request, [
67 'name' => ['required', 'string', 'max:255'],
70 $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
71 $this->checkOwnablePermission('page-create', $parent);
73 $page = $this->pageRepo->getNewDraftPage($parent);
74 $this->pageRepo->publishDraft($page, [
75 'name' => $request->get('name'),
78 return redirect($page->getUrl('/edit'));
82 * Show form to continue editing a draft page.
84 * @throws NotFoundException
86 public function editDraft(Request $request, string $bookSlug, int $pageId)
88 $draft = $this->pageRepo->getById($pageId);
89 $this->checkOwnablePermission('page-create', $draft->getParent());
91 $editorData = new PageEditorData($draft, $this->pageRepo, $request->query('editor', ''));
92 $this->setPageTitle(trans('entities.pages_edit_draft'));
94 return view('pages.edit', $editorData->getViewData());
98 * Store a new page by changing a draft into a page.
100 * @throws NotFoundException
101 * @throws ValidationException
103 public function store(Request $request, string $bookSlug, int $pageId)
105 $this->validate($request, [
106 'name' => ['required', 'string', 'max:255'],
108 $draftPage = $this->pageRepo->getById($pageId);
109 $this->checkOwnablePermission('page-create', $draftPage->getParent());
111 $page = $this->pageRepo->publishDraft($draftPage, $request->all());
113 return redirect($page->getUrl());
117 * Display the specified page.
118 * If the page is not found via the slug the revisions are searched for a match.
120 * @throws NotFoundException
122 public function show(string $bookSlug, string $pageSlug)
125 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
126 } catch (NotFoundException $e) {
127 $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
129 if ($page === null) {
133 return redirect($page->getUrl());
136 $this->checkOwnablePermission('page-view', $page);
138 $pageContent = (new PageContent($page));
139 $page->html = $pageContent->render();
140 $pageNav = $pageContent->getNavigation($page->html);
142 $sidebarTree = (new BookContents($page->book))->getTree();
143 $commentTree = (new CommentTree($page));
144 $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
146 View::incrementFor($page);
147 $this->setPageTitle($page->getShortName());
149 return view('pages.show', [
151 'book' => $page->book,
153 'sidebarTree' => $sidebarTree,
154 'commentTree' => $commentTree,
155 'pageNav' => $pageNav,
156 'watchOptions' => new UserEntityWatchOptions(user(), $page),
157 'next' => $nextPreviousLocator->getNext(),
158 'previous' => $nextPreviousLocator->getPrevious(),
159 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($page),
164 * Get page from an ajax request.
166 * @throws NotFoundException
168 public function getPageAjax(int $pageId)
170 $page = $this->pageRepo->getById($pageId);
171 $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
172 $page->makeHidden(['book']);
174 return response()->json($page);
178 * Show the form for editing the specified page.
180 * @throws NotFoundException
182 public function edit(Request $request, string $bookSlug, string $pageSlug)
184 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
185 $this->checkOwnablePermission('page-update', $page);
187 $editorData = new PageEditorData($page, $this->pageRepo, $request->query('editor', ''));
188 if ($editorData->getWarnings()) {
189 $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
192 $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
194 return view('pages.edit', $editorData->getViewData());
198 * Update the specified page in storage.
200 * @throws ValidationException
201 * @throws NotFoundException
203 public function update(Request $request, string $bookSlug, string $pageSlug)
205 $this->validate($request, [
206 'name' => ['required', 'string', 'max:255'],
208 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
209 $this->checkOwnablePermission('page-update', $page);
211 $this->pageRepo->update($page, $request->all());
213 return redirect($page->getUrl());
217 * Save a draft update as a revision.
219 * @throws NotFoundException
221 public function saveDraft(Request $request, int $pageId)
223 $page = $this->pageRepo->getById($pageId);
224 $this->checkOwnablePermission('page-update', $page);
226 if (!$this->isSignedIn()) {
227 return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
230 $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
231 $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
233 return response()->json([
234 'status' => 'success',
235 'message' => trans('entities.pages_edit_draft_save_at'),
236 'warning' => implode("\n", $warnings),
237 'timestamp' => $draft->updated_at->timestamp,
242 * Redirect from a special link url which uses the page id rather than the name.
244 * @throws NotFoundException
246 public function redirectFromLink(int $pageId)
248 $page = $this->pageRepo->getById($pageId);
250 return redirect($page->getUrl());
254 * Show the deletion page for the specified page.
256 * @throws NotFoundException
258 public function showDelete(string $bookSlug, string $pageSlug)
260 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
261 $this->checkOwnablePermission('page-delete', $page);
262 $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
264 Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
265 Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
267 return view('pages.delete', [
268 'book' => $page->book,
271 'usedAsTemplate' => $usedAsTemplate,
276 * Show the deletion page for the specified page.
278 * @throws NotFoundException
280 public function showDeleteDraft(string $bookSlug, int $pageId)
282 $page = $this->pageRepo->getById($pageId);
283 $this->checkOwnablePermission('page-update', $page);
284 $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
286 Book::query()->where('default_template_id', '=', $page->id)->count() > 0 ||
287 Chapter::query()->where('default_template_id', '=', $page->id)->count() > 0;
289 return view('pages.delete', [
290 'book' => $page->book,
293 'usedAsTemplate' => $usedAsTemplate,
298 * Remove the specified page from storage.
300 * @throws NotFoundException
303 public function destroy(string $bookSlug, string $pageSlug)
305 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
306 $this->checkOwnablePermission('page-delete', $page);
307 $parent = $page->getParent();
309 $this->pageRepo->destroy($page);
311 return redirect($parent->getUrl());
315 * Remove the specified draft page from storage.
317 * @throws NotFoundException
320 public function destroyDraft(string $bookSlug, int $pageId)
322 $page = $this->pageRepo->getById($pageId);
324 $chapter = $page->chapter;
325 $this->checkOwnablePermission('page-update', $page);
327 $this->pageRepo->destroy($page);
329 $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
331 if ($chapter && userCan('view', $chapter)) {
332 return redirect($chapter->getUrl());
335 return redirect($book->getUrl());
339 * Show a listing of recently created pages.
341 public function showRecentlyUpdated()
343 $visibleBelongsScope = function (BelongsTo $query) {
344 $query->scopes('visible');
347 $pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
348 ->orderBy('updated_at', 'desc')
350 ->setPath(url('/pages/recently-updated'));
352 $this->setPageTitle(trans('entities.recently_updated_pages'));
354 return view('common.detailed-listing-paginated', [
355 'title' => trans('entities.recently_updated_pages'),
356 'entities' => $pages,
357 'showUpdatedBy' => true,
363 * Show the view to choose a new parent to move a page into.
365 * @throws NotFoundException
367 public function showMove(string $bookSlug, string $pageSlug)
369 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
370 $this->checkOwnablePermission('page-update', $page);
371 $this->checkOwnablePermission('page-delete', $page);
373 return view('pages.move', [
374 'book' => $page->book,
380 * Does the action of moving the location of a page.
382 * @throws NotFoundException
385 public function move(Request $request, string $bookSlug, string $pageSlug)
387 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
388 $this->checkOwnablePermission('page-update', $page);
389 $this->checkOwnablePermission('page-delete', $page);
391 $entitySelection = $request->get('entity_selection', null);
392 if ($entitySelection === null || $entitySelection === '') {
393 return redirect($page->getUrl());
397 $this->pageRepo->move($page, $entitySelection);
398 } catch (PermissionsException $exception) {
399 $this->showPermissionError();
400 } catch (Exception $exception) {
401 $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
403 return redirect($page->getUrl('/move'));
406 return redirect($page->getUrl());
410 * Show the view to copy a page.
412 * @throws NotFoundException
414 public function showCopy(string $bookSlug, string $pageSlug)
416 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
417 $this->checkOwnablePermission('page-view', $page);
418 session()->flashInput(['name' => $page->name]);
420 return view('pages.copy', [
421 'book' => $page->book,
427 * Create a copy of a page within the requested target destination.
429 * @throws NotFoundException
432 public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
434 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
435 $this->checkOwnablePermission('page-view', $page);
437 $entitySelection = $request->get('entity_selection') ?: null;
438 $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
440 if (is_null($newParent)) {
441 $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
443 return redirect($page->getUrl('/copy'));
446 $this->checkOwnablePermission('page-create', $newParent);
448 $newName = $request->get('name') ?: $page->name;
449 $pageCopy = $cloner->clonePage($page, $newParent, $newName);
450 $this->showSuccessNotification(trans('entities.pages_copy_success'));
452 return redirect($pageCopy->getUrl());