1 <?php namespace BookStack\Http\Controllers;
4 use BookStack\Auth\UserRepo;
5 use BookStack\Entities\Repos\PageRepo;
6 use BookStack\Exceptions\NotFoundException;
8 use GatherContent\Htmldiff\Htmldiff;
9 use Illuminate\Contracts\View\Factory;
10 use Illuminate\Http\JsonResponse;
11 use Illuminate\Http\RedirectResponse;
12 use Illuminate\Http\Request;
13 use Illuminate\Http\Response;
14 use Illuminate\Routing\Redirector;
15 use Illuminate\View\View;
19 class PageController extends Controller
26 * PageController constructor.
27 * @param PageRepo $pageRepo
28 * @param UserRepo $userRepo
30 public function __construct(PageRepo $pageRepo, UserRepo $userRepo)
32 $this->pageRepo = $pageRepo;
33 $this->userRepo = $userRepo;
34 parent::__construct();
38 * Show the form for creating a new page.
39 * @param string $bookSlug
40 * @param string $chapterSlug
42 * @internal param bool $pageSlug
43 * @throws NotFoundException
45 public function create($bookSlug, $chapterSlug = null)
47 if ($chapterSlug !== null) {
48 $chapter = $this->pageRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
49 $book = $chapter->book;
52 $book = $this->pageRepo->getEntityBySlug('book', $bookSlug);
55 $parent = $chapter ? $chapter : $book;
56 $this->checkOwnablePermission('page-create', $parent);
58 // Redirect to draft edit screen if signed in
59 if ($this->signedIn) {
60 $draft = $this->pageRepo->getDraftPage($book, $chapter);
61 return redirect($draft->getUrl());
64 // Otherwise show the edit view if they're a guest
65 $this->setPageTitle(trans('entities.pages_new'));
66 return view('pages.guest-create', ['parent' => $parent]);
70 * Create a new page as a guest user.
71 * @param Request $request
72 * @param string $bookSlug
73 * @param string|null $chapterSlug
75 * @throws NotFoundException
77 public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
79 $this->validate($request, [
80 'name' => 'required|string|max:255'
83 if ($chapterSlug !== null) {
84 $chapter = $this->pageRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
85 $book = $chapter->book;
88 $book = $this->pageRepo->getEntityBySlug('book', $bookSlug);
91 $parent = $chapter ? $chapter : $book;
92 $this->checkOwnablePermission('page-create', $parent);
94 $page = $this->pageRepo->getDraftPage($book, $chapter);
95 $this->pageRepo->publishPageDraft($page, [
96 'name' => $request->get('name'),
99 return redirect($page->getUrl('/edit'));
103 * Show form to continue editing a draft page.
104 * @param string $bookSlug
106 * @return Factory|View
108 public function editDraft($bookSlug, $pageId)
110 $draft = $this->pageRepo->getById('page', $pageId, true);
111 $this->checkOwnablePermission('page-create', $draft->parent);
112 $this->setPageTitle(trans('entities.pages_edit_draft'));
114 $draftsEnabled = $this->signedIn;
115 $templates = $this->pageRepo->getPageTemplates(10);
117 return view('pages.edit', [
119 'book' => $draft->book,
121 'draftsEnabled' => $draftsEnabled,
122 'templates' => $templates,
127 * Store a new page by changing a draft into a page.
128 * @param Request $request
129 * @param string $bookSlug
133 public function store(Request $request, $bookSlug, $pageId)
135 $this->validate($request, [
136 'name' => 'required|string|max:255'
139 $input = $request->all();
140 $draftPage = $this->pageRepo->getById('page', $pageId, true);
141 $book = $draftPage->book;
143 $parent = $draftPage->parent;
144 $this->checkOwnablePermission('page-create', $parent);
146 if ($parent->isA('chapter')) {
147 $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
149 $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
152 $page = $this->pageRepo->publishPageDraft($draftPage, $input);
154 Activity::add($page, 'page_create', $book->id);
155 return redirect($page->getUrl());
159 * Display the specified page.
160 * If the page is not found via the slug the revisions are searched for a match.
161 * @param string $bookSlug
162 * @param string $pageSlug
164 * @throws NotFoundException
166 public function show($bookSlug, $pageSlug)
169 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
170 } catch (NotFoundException $e) {
171 $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
172 if ($page === null) {
175 return redirect($page->getUrl());
178 $this->checkOwnablePermission('page-view', $page);
180 $page->html = $this->pageRepo->renderPage($page);
181 $sidebarTree = $this->pageRepo->getBookChildren($page->book);
182 $pageNav = $this->pageRepo->getPageNav($page->html);
184 // check if the comment's are enabled
185 $commentsEnabled = !setting('app-disable-comments');
186 if ($commentsEnabled) {
187 $page->load(['comments.createdBy']);
191 $this->setPageTitle($page->getShortName());
192 return view('pages.show', [
193 'page' => $page,'book' => $page->book,
195 'sidebarTree' => $sidebarTree,
196 'commentsEnabled' => $commentsEnabled,
197 'pageNav' => $pageNav
202 * Get page from an ajax request.
204 * @return JsonResponse
206 public function getPageAjax($pageId)
208 $page = $this->pageRepo->getById('page', $pageId);
209 return response()->json($page);
213 * Show the form for editing the specified page.
214 * @param string $bookSlug
215 * @param string $pageSlug
217 * @throws NotFoundException
219 public function edit($bookSlug, $pageSlug)
221 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
222 $this->checkOwnablePermission('page-update', $page);
223 $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
224 $page->isDraft = false;
226 // Check for active editing
228 if ($this->pageRepo->isPageEditingActive($page, 60)) {
229 $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
232 // Check for a current draft version for this user
233 $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
234 if ($userPageDraft !== null) {
235 $page->name = $userPageDraft->name;
236 $page->html = $userPageDraft->html;
237 $page->markdown = $userPageDraft->markdown;
238 $page->isDraft = true;
239 $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
242 if (count($warnings) > 0) {
243 session()->flash('warning', implode("\n", $warnings));
246 $draftsEnabled = $this->signedIn;
247 $templates = $this->pageRepo->getPageTemplates(10);
249 return view('pages.edit', [
251 'book' => $page->book,
253 'draftsEnabled' => $draftsEnabled,
254 'templates' => $templates,
259 * Update the specified page in storage.
260 * @param Request $request
261 * @param string $bookSlug
262 * @param string $pageSlug
265 public function update(Request $request, $bookSlug, $pageSlug)
267 $this->validate($request, [
268 'name' => 'required|string|max:255'
270 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
271 $this->checkOwnablePermission('page-update', $page);
272 $this->pageRepo->updatePage($page, $page->book->id, $request->all());
273 Activity::add($page, 'page_update', $page->book->id);
274 return redirect($page->getUrl());
278 * Save a draft update as a revision.
279 * @param Request $request
281 * @return JsonResponse
283 public function saveDraft(Request $request, $pageId)
285 $page = $this->pageRepo->getById('page', $pageId, true);
286 $this->checkOwnablePermission('page-update', $page);
288 if (!$this->signedIn) {
289 return response()->json([
291 'message' => trans('errors.guests_cannot_save_drafts'),
295 $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
297 $updateTime = $draft->updated_at->timestamp;
298 return response()->json([
299 'status' => 'success',
300 'message' => trans('entities.pages_edit_draft_save_at'),
301 'timestamp' => $updateTime
306 * Redirect from a special link url which
307 * uses the page id rather than the name.
309 * @return RedirectResponse|Redirector
311 public function redirectFromLink($pageId)
313 $page = $this->pageRepo->getById('page', $pageId);
314 return redirect($page->getUrl());
318 * Show the deletion page for the specified page.
319 * @param string $bookSlug
320 * @param string $pageSlug
323 public function showDelete($bookSlug, $pageSlug)
325 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
326 $this->checkOwnablePermission('page-delete', $page);
327 $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
328 return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
333 * Show the deletion page for the specified page.
334 * @param string $bookSlug
337 * @throws NotFoundException
339 public function showDeleteDraft($bookSlug, $pageId)
341 $page = $this->pageRepo->getById('page', $pageId, true);
342 $this->checkOwnablePermission('page-update', $page);
343 $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
344 return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
348 * Remove the specified page from storage.
349 * @param string $bookSlug
350 * @param string $pageSlug
352 * @internal param int $id
354 public function destroy($bookSlug, $pageSlug)
356 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
358 $this->checkOwnablePermission('page-delete', $page);
359 $this->pageRepo->destroyPage($page);
361 Activity::addMessage('page_delete', $book->id, $page->name);
362 session()->flash('success', trans('entities.pages_delete_success'));
363 return redirect($book->getUrl());
367 * Remove the specified draft page from storage.
368 * @param string $bookSlug
371 * @throws NotFoundException
373 public function destroyDraft($bookSlug, $pageId)
375 $page = $this->pageRepo->getById('page', $pageId, true);
377 $this->checkOwnablePermission('page-update', $page);
378 session()->flash('success', trans('entities.pages_delete_draft_success'));
379 $this->pageRepo->destroyPage($page);
380 return redirect($book->getUrl());
384 * Shows the last revisions for this page.
385 * @param string $bookSlug
386 * @param string $pageSlug
388 * @throws NotFoundException
390 public function showRevisions($bookSlug, $pageSlug)
392 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
393 $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
394 return view('pages.revisions', ['page' => $page, 'current' => $page]);
398 * Shows a preview of a single revision
399 * @param string $bookSlug
400 * @param string $pageSlug
401 * @param int $revisionId
404 public function showRevision($bookSlug, $pageSlug, $revisionId)
406 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
407 $revision = $page->revisions()->where('id', '=', $revisionId)->first();
408 if ($revision === null) {
412 $page->fill($revision->toArray());
413 $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
415 return view('pages.revision', [
417 'book' => $page->book,
419 'revision' => $revision
424 * Shows the changes of a single revision
425 * @param string $bookSlug
426 * @param string $pageSlug
427 * @param int $revisionId
430 public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
432 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
433 $revision = $page->revisions()->where('id', '=', $revisionId)->first();
434 if ($revision === null) {
438 $prev = $revision->getPrevious();
439 $prevContent = ($prev === null) ? '' : $prev->html;
440 $diff = (new Htmldiff)->diff($prevContent, $revision->html);
442 $page->fill($revision->toArray());
443 $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
445 return view('pages.revision', [
447 'book' => $page->book,
449 'revision' => $revision
454 * Restores a page using the content of the specified revision.
455 * @param string $bookSlug
456 * @param string $pageSlug
457 * @param int $revisionId
458 * @return RedirectResponse|Redirector
460 public function restoreRevision($bookSlug, $pageSlug, $revisionId)
462 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
463 $this->checkOwnablePermission('page-update', $page);
464 $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
465 Activity::add($page, 'page_restore', $page->book->id);
466 return redirect($page->getUrl());
471 * Deletes a revision using the id of the specified revision.
472 * @param string $bookSlug
473 * @param string $pageSlug
475 * @return RedirectResponse|Redirector
476 *@throws BadRequestException
477 * @throws NotFoundException
479 public function destroyRevision($bookSlug, $pageSlug, $revId)
481 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
482 $this->checkOwnablePermission('page-delete', $page);
484 $revision = $page->revisions()->where('id', '=', $revId)->first();
485 if ($revision === null) {
486 throw new NotFoundException("Revision #{$revId} not found");
489 // Get the current revision for the page
490 $currentRevision = $page->getCurrentRevision();
492 // Check if its the latest revision, cannot delete latest revision.
493 if (intval($currentRevision->id) === intval($revId)) {
494 session()->flash('error', trans('entities.revision_cannot_delete_latest'));
495 return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
499 session()->flash('success', trans('entities.revision_delete_success'));
500 return redirect($page->getUrl('/revisions'));
504 * Show a listing of recently created pages
505 * @return Factory|View
507 public function showRecentlyUpdated()
509 // TODO - Still exist?
510 $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(url('/pages/recently-updated'));
511 return view('pages.detailed-listing', [
512 'title' => trans('entities.recently_updated_pages'),
518 * Show the view to choose a new parent to move a page into.
519 * @param string $bookSlug
520 * @param string $pageSlug
522 * @throws NotFoundException
524 public function showMove($bookSlug, $pageSlug)
526 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
527 $this->checkOwnablePermission('page-update', $page);
528 $this->checkOwnablePermission('page-delete', $page);
529 return view('pages.move', [
530 'book' => $page->book,
536 * Does the action of moving the location of a page
537 * @param Request $request
538 * @param string $bookSlug
539 * @param string $pageSlug
541 * @throws NotFoundException
544 public function move(Request $request, string $bookSlug, string $pageSlug)
546 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
547 $this->checkOwnablePermission('page-update', $page);
548 $this->checkOwnablePermission('page-delete', $page);
550 $entitySelection = $request->get('entity_selection', null);
551 if ($entitySelection === null || $entitySelection === '') {
552 return redirect($page->getUrl());
555 $stringExploded = explode(':', $entitySelection);
556 $entityType = $stringExploded[0];
557 $entityId = intval($stringExploded[1]);
561 $parent = $this->pageRepo->getById($entityType, $entityId);
562 } catch (Exception $e) {
563 session()->flash(trans('entities.selected_book_chapter_not_found'));
564 return redirect()->back();
567 $this->checkOwnablePermission('page-create', $parent);
569 $this->pageRepo->changePageParent($page, $parent);
570 Activity::add($page, 'page_move', $page->book->id);
571 session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
573 return redirect($page->getUrl());
577 * Show the view to copy a page.
578 * @param string $bookSlug
579 * @param string $pageSlug
581 * @throws NotFoundException
583 public function showCopy($bookSlug, $pageSlug)
585 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
586 $this->checkOwnablePermission('page-view', $page);
587 session()->flashInput(['name' => $page->name]);
588 return view('pages.copy', [
589 'book' => $page->book,
595 * Create a copy of a page within the requested target destination.
596 * @param Request $request
597 * @param string $bookSlug
598 * @param string $pageSlug
600 * @throws NotFoundException
603 public function copy(Request $request, string $bookSlug, string $pageSlug)
605 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
606 $this->checkOwnablePermission('page-view', $page);
608 $entitySelection = $request->get('entity_selection', null);
609 if ($entitySelection === null || $entitySelection === '') {
610 $parent = $page->chapter ? $page->chapter : $page->book;
612 $stringExploded = explode(':', $entitySelection);
613 $entityType = $stringExploded[0];
614 $entityId = intval($stringExploded[1]);
617 $parent = $this->pageRepo->getById($entityType, $entityId);
618 } catch (Exception $e) {
619 session()->flash(trans('entities.selected_book_chapter_not_found'));
620 return redirect()->back();
624 $this->checkOwnablePermission('page-create', $parent);
626 $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
628 Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
629 session()->flash('success', trans('entities.pages_copy_success'));
631 return redirect($pageCopy->getUrl());
635 * Show the Permissions view.
636 * @param string $bookSlug
637 * @param string $pageSlug
638 * @return Factory|View
639 * @throws NotFoundException
641 public function showPermissions($bookSlug, $pageSlug)
643 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
644 $this->checkOwnablePermission('restrictions-manage', $page);
645 $roles = $this->userRepo->getRestrictableRoles();
646 return view('pages.permissions', [
653 * Set the permissions for this page.
654 * @param string $bookSlug
655 * @param string $pageSlug
656 * @param Request $request
657 * @return RedirectResponse|Redirector
658 * @throws NotFoundException
661 public function permissions(Request $request, string $bookSlug, string $pageSlug)
663 $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
664 $this->checkOwnablePermission('restrictions-manage', $page);
665 $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
666 session()->flash('success', trans('entities.pages_permissions_success'));
667 return redirect($page->getUrl());