3 namespace BookStack\Http\Controllers;
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\PermissionsUpdater;
14 use BookStack\Exceptions\NotFoundException;
15 use BookStack\Exceptions\PermissionsException;
17 use Illuminate\Database\Eloquent\Relations\BelongsTo;
18 use Illuminate\Http\Request;
19 use Illuminate\Validation\ValidationException;
22 class PageController extends Controller
27 * PageController constructor.
29 public function __construct(PageRepo $pageRepo)
31 $this->pageRepo = $pageRepo;
35 * Show the form for creating a new page.
39 public function create(string $bookSlug, string $chapterSlug = null)
41 $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
42 $this->checkOwnablePermission('page-create', $parent);
44 // Redirect to draft edit screen if signed in
45 if ($this->isSignedIn()) {
46 $draft = $this->pageRepo->getNewDraftPage($parent);
48 return redirect($draft->getUrl());
51 // Otherwise show the edit view if they're a guest
52 $this->setPageTitle(trans('entities.pages_new'));
54 return view('pages.guest-create', ['parent' => $parent]);
58 * Create a new page as a guest user.
60 * @throws ValidationException
62 public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
64 $this->validate($request, [
65 'name' => ['required', 'string', 'max:255'],
68 $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
69 $this->checkOwnablePermission('page-create', $parent);
71 $page = $this->pageRepo->getNewDraftPage($parent);
72 $this->pageRepo->publishDraft($page, [
73 'name' => $request->get('name'),
77 return redirect($page->getUrl('/edit'));
81 * Show form to continue editing a draft page.
83 * @throws NotFoundException
85 public function editDraft(string $bookSlug, int $pageId)
87 $draft = $this->pageRepo->getById($pageId);
88 $this->checkOwnablePermission('page-create', $draft->getParent());
89 $this->setPageTitle(trans('entities.pages_edit_draft'));
91 $draftsEnabled = $this->isSignedIn();
92 $templates = $this->pageRepo->getTemplates(10);
94 return view('pages.edit', [
96 'book' => $draft->book,
98 'draftsEnabled' => $draftsEnabled,
99 'templates' => $templates,
104 * Store a new page by changing a draft into a page.
106 * @throws NotFoundException
107 * @throws ValidationException
109 public function store(Request $request, string $bookSlug, int $pageId)
111 $this->validate($request, [
112 'name' => ['required', 'string', 'max:255'],
114 $draftPage = $this->pageRepo->getById($pageId);
115 $this->checkOwnablePermission('page-create', $draftPage->getParent());
117 $page = $this->pageRepo->publishDraft($draftPage, $request->all());
119 return redirect($page->getUrl());
123 * Display the specified page.
124 * If the page is not found via the slug the revisions are searched for a match.
126 * @throws NotFoundException
128 public function show(string $bookSlug, string $pageSlug)
131 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
132 } catch (NotFoundException $e) {
133 $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
135 if ($page === null) {
139 return redirect($page->getUrl());
142 $this->checkOwnablePermission('page-view', $page);
144 $pageContent = (new PageContent($page));
145 $page->html = $pageContent->render();
146 $sidebarTree = (new BookContents($page->book))->getTree();
147 $pageNav = $pageContent->getNavigation($page->html);
149 // Check if page comments are enabled
150 $commentsEnabled = !setting('app-disable-comments');
151 if ($commentsEnabled) {
152 $page->load(['comments.createdBy']);
155 $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
157 View::incrementFor($page);
158 $this->setPageTitle($page->getShortName());
160 return view('pages.show', [
162 'book' => $page->book,
164 'sidebarTree' => $sidebarTree,
165 'commentsEnabled' => $commentsEnabled,
166 'pageNav' => $pageNav,
167 'next' => $nextPreviousLocator->getNext(),
168 'previous' => $nextPreviousLocator->getPrevious(),
173 * Get page from an ajax request.
175 * @throws NotFoundException
177 public function getPageAjax(int $pageId)
179 $page = $this->pageRepo->getById($pageId);
180 $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
181 $page->makeHidden(['book']);
183 return response()->json($page);
187 * Show the form for editing the specified page.
189 * @throws NotFoundException
191 public function edit(string $bookSlug, string $pageSlug)
193 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
194 $this->checkOwnablePermission('page-update', $page);
196 $page->isDraft = false;
197 $editActivity = new PageEditActivity($page);
199 // Check for active editing
201 if ($editActivity->hasActiveEditing()) {
202 $warnings[] = $editActivity->activeEditingMessage();
205 // Check for a current draft version for this user
206 $userDraft = $this->pageRepo->getUserDraft($page);
207 if ($userDraft !== null) {
208 $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
209 $page->isDraft = true;
210 $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
213 if (count($warnings) > 0) {
214 $this->showWarningNotification(implode("\n", $warnings));
217 $templates = $this->pageRepo->getTemplates(10);
218 $draftsEnabled = $this->isSignedIn();
219 $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
221 return view('pages.edit', [
223 'book' => $page->book,
225 'draftsEnabled' => $draftsEnabled,
226 'templates' => $templates,
231 * Update the specified page in storage.
233 * @throws ValidationException
234 * @throws NotFoundException
236 public function update(Request $request, string $bookSlug, string $pageSlug)
238 $this->validate($request, [
239 'name' => ['required', 'string', 'max:255'],
241 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
242 $this->checkOwnablePermission('page-update', $page);
244 $this->pageRepo->update($page, $request->all());
246 return redirect($page->getUrl());
250 * Save a draft update as a revision.
252 * @throws NotFoundException
254 public function saveDraft(Request $request, int $pageId)
256 $page = $this->pageRepo->getById($pageId);
257 $this->checkOwnablePermission('page-update', $page);
259 if (!$this->isSignedIn()) {
260 return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
263 $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
264 $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
266 return response()->json([
267 'status' => 'success',
268 'message' => trans('entities.pages_edit_draft_save_at'),
269 'warning' => implode("\n", $warnings),
270 'timestamp' => $draft->updated_at->timestamp,
275 * Redirect from a special link url which uses the page id rather than the name.
277 * @throws NotFoundException
279 public function redirectFromLink(int $pageId)
281 $page = $this->pageRepo->getById($pageId);
283 return redirect($page->getUrl());
287 * Show the deletion page for the specified page.
289 * @throws NotFoundException
291 public function showDelete(string $bookSlug, string $pageSlug)
293 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
294 $this->checkOwnablePermission('page-delete', $page);
295 $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
297 return view('pages.delete', [
298 'book' => $page->book,
305 * Show the deletion page for the specified page.
307 * @throws NotFoundException
309 public function showDeleteDraft(string $bookSlug, int $pageId)
311 $page = $this->pageRepo->getById($pageId);
312 $this->checkOwnablePermission('page-update', $page);
313 $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
315 return view('pages.delete', [
316 'book' => $page->book,
323 * Remove the specified page from storage.
325 * @throws NotFoundException
328 public function destroy(string $bookSlug, string $pageSlug)
330 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
331 $this->checkOwnablePermission('page-delete', $page);
332 $parent = $page->getParent();
334 $this->pageRepo->destroy($page);
336 return redirect($parent->getUrl());
340 * Remove the specified draft page from storage.
342 * @throws NotFoundException
345 public function destroyDraft(string $bookSlug, int $pageId)
347 $page = $this->pageRepo->getById($pageId);
349 $chapter = $page->chapter;
350 $this->checkOwnablePermission('page-update', $page);
352 $this->pageRepo->destroy($page);
354 $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
356 if ($chapter && userCan('view', $chapter)) {
357 return redirect($chapter->getUrl());
360 return redirect($book->getUrl());
364 * Show a listing of recently created pages.
366 public function showRecentlyUpdated()
368 $visibleBelongsScope = function (BelongsTo $query) {
369 $query->scopes('visible');
372 $pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
373 ->orderBy('updated_at', 'desc')
375 ->setPath(url('/pages/recently-updated'));
377 $this->setPageTitle(trans('entities.recently_updated_pages'));
379 return view('common.detailed-listing-paginated', [
380 'title' => trans('entities.recently_updated_pages'),
381 'entities' => $pages,
382 'showUpdatedBy' => true,
388 * Show the view to choose a new parent to move a page into.
390 * @throws NotFoundException
392 public function showMove(string $bookSlug, string $pageSlug)
394 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
395 $this->checkOwnablePermission('page-update', $page);
396 $this->checkOwnablePermission('page-delete', $page);
398 return view('pages.move', [
399 'book' => $page->book,
405 * Does the action of moving the location of a page.
407 * @throws NotFoundException
410 public function move(Request $request, string $bookSlug, string $pageSlug)
412 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
413 $this->checkOwnablePermission('page-update', $page);
414 $this->checkOwnablePermission('page-delete', $page);
416 $entitySelection = $request->get('entity_selection', null);
417 if ($entitySelection === null || $entitySelection === '') {
418 return redirect($page->getUrl());
422 $parent = $this->pageRepo->move($page, $entitySelection);
423 } catch (PermissionsException $exception) {
424 $this->showPermissionError();
425 } catch (Exception $exception) {
426 $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
428 return redirect()->back();
431 $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
433 return redirect($page->getUrl());
437 * Show the view to copy a page.
439 * @throws NotFoundException
441 public function showCopy(string $bookSlug, string $pageSlug)
443 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
444 $this->checkOwnablePermission('page-view', $page);
445 session()->flashInput(['name' => $page->name]);
447 return view('pages.copy', [
448 'book' => $page->book,
454 * Create a copy of a page within the requested target destination.
456 * @throws NotFoundException
459 public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
461 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
462 $this->checkOwnablePermission('page-view', $page);
464 $entitySelection = $request->get('entity_selection') ?: null;
465 $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
467 if (is_null($newParent)) {
468 $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
470 return redirect()->back();
473 $this->checkOwnablePermission('page-create', $newParent);
475 $newName = $request->get('name') ?: $page->name;
476 $pageCopy = $cloner->clonePage($page, $newParent, $newName);
477 $this->showSuccessNotification(trans('entities.pages_copy_success'));
479 return redirect($pageCopy->getUrl());
483 * Show the Permissions view.
485 * @throws NotFoundException
487 public function showPermissions(string $bookSlug, string $pageSlug)
489 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
490 $this->checkOwnablePermission('restrictions-manage', $page);
492 return view('pages.permissions', [
498 * Set the permissions for this page.
500 * @throws NotFoundException
503 public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $pageSlug)
505 $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
506 $this->checkOwnablePermission('restrictions-manage', $page);
508 $permissionsUpdater->updateFromPermissionsForm($page, $request);
510 $this->showSuccessNotification(trans('entities.pages_permissions_success'));
512 return redirect($page->getUrl());