1 <?php namespace BookStack\Http\Controllers;
4 use BookStack\Exceptions\NotFoundException;
5 use BookStack\Repos\EntityRepo;
6 use BookStack\Repos\UserRepo;
7 use BookStack\Services\ExportService;
8 use Illuminate\Http\Request;
9 use Illuminate\Http\Response;
11 use GatherContent\Htmldiff\Htmldiff;
13 class PageController extends Controller
16 protected $entityRepo;
17 protected $exportService;
21 * PageController constructor.
22 * @param EntityRepo $entityRepo
23 * @param ExportService $exportService
24 * @param UserRepo $userRepo
26 public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
28 $this->entityRepo = $entityRepo;
29 $this->exportService = $exportService;
30 $this->userRepo = $userRepo;
31 parent::__construct();
35 * Show the form for creating a new page.
36 * @param string $bookSlug
37 * @param string $chapterSlug
39 * @internal param bool $pageSlug
40 * @throws NotFoundException
42 public function create($bookSlug, $chapterSlug = null)
44 if ($chapterSlug !== null) {
45 $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
46 $book = $chapter->book;
49 $book = $this->entityRepo->getBySlug('book', $bookSlug);
52 $parent = $chapter ? $chapter : $book;
53 $this->checkOwnablePermission('page-create', $parent);
55 // Redirect to draft edit screen if signed in
56 if ($this->signedIn) {
57 $draft = $this->entityRepo->getDraftPage($book, $chapter);
58 return redirect($draft->getUrl());
61 // Otherwise show the edit view if they're a guest
62 $this->setPageTitle(trans('entities.pages_new'));
63 return view('pages/guest-create', ['parent' => $parent]);
67 * Create a new page as a guest user.
68 * @param Request $request
69 * @param string $bookSlug
70 * @param string|null $chapterSlug
72 * @throws NotFoundException
74 public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
76 $this->validate($request, [
77 'name' => 'required|string|max:255'
80 if ($chapterSlug !== null) {
81 $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
82 $book = $chapter->book;
85 $book = $this->entityRepo->getBySlug('book', $bookSlug);
88 $parent = $chapter ? $chapter : $book;
89 $this->checkOwnablePermission('page-create', $parent);
91 $page = $this->entityRepo->getDraftPage($book, $chapter);
92 $this->entityRepo->publishPageDraft($page, [
93 'name' => $request->get('name'),
96 return redirect($page->getUrl('/edit'));
100 * Show form to continue editing a draft page.
101 * @param string $bookSlug
103 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
105 public function editDraft($bookSlug, $pageId)
107 $draft = $this->entityRepo->getById('page', $pageId, true);
108 $this->checkOwnablePermission('page-create', $draft->parent);
109 $this->setPageTitle(trans('entities.pages_edit_draft'));
111 $draftsEnabled = $this->signedIn;
112 return view('pages/edit', [
114 'book' => $draft->book,
116 'draftsEnabled' => $draftsEnabled
121 * Store a new page by changing a draft into a page.
122 * @param Request $request
123 * @param string $bookSlug
127 public function store(Request $request, $bookSlug, $pageId)
129 $this->validate($request, [
130 'name' => 'required|string|max:255'
133 $input = $request->all();
134 $draftPage = $this->entityRepo->getById('page', $pageId, true);
135 $book = $draftPage->book;
137 $parent = $draftPage->parent;
138 $this->checkOwnablePermission('page-create', $parent);
140 if ($parent->isA('chapter')) {
141 $input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
143 $input['priority'] = $this->entityRepo->getNewBookPriority($parent);
146 $page = $this->entityRepo->publishPageDraft($draftPage, $input);
148 Activity::add($page, 'page_create', $book->id);
149 return redirect($page->getUrl());
153 * Display the specified page.
154 * If the page is not found via the slug the revisions are searched for a match.
155 * @param string $bookSlug
156 * @param string $pageSlug
158 * @throws NotFoundException
160 public function show($bookSlug, $pageSlug)
163 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
164 } catch (NotFoundException $e) {
165 $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
166 if ($page === null) {
169 return redirect($page->getUrl());
172 $this->checkOwnablePermission('page-view', $page);
174 $page->html = $this->entityRepo->renderPage($page);
175 $sidebarTree = $this->entityRepo->getBookChildren($page->book);
176 $pageNav = $this->entityRepo->getPageNav($page->html);
178 // check if the comment's are enabled
179 $commentsEnabled = !setting('app-disable-comments');
180 if ($commentsEnabled) {
181 $page->load(['comments.createdBy']);
185 $this->setPageTitle($page->getShortName());
186 return view('pages/show', [
187 'page' => $page,'book' => $page->book,
189 'sidebarTree' => $sidebarTree,
190 'commentsEnabled' => $commentsEnabled,
191 'pageNav' => $pageNav
196 * Get page from an ajax request.
198 * @return \Illuminate\Http\JsonResponse
200 public function getPageAjax($pageId)
202 $page = $this->entityRepo->getById('page', $pageId);
203 return response()->json($page);
207 * Show the form for editing the specified page.
208 * @param string $bookSlug
209 * @param string $pageSlug
212 public function edit($bookSlug, $pageSlug)
214 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
215 $this->checkOwnablePermission('page-update', $page);
216 $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
217 $page->isDraft = false;
219 // Check for active editing
221 if ($this->entityRepo->isPageEditingActive($page, 60)) {
222 $warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
225 // Check for a current draft version for this user
226 if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
227 $draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
228 $page->name = $draft->name;
229 $page->html = $draft->html;
230 $page->markdown = $draft->markdown;
231 $page->isDraft = true;
232 $warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
235 if (count($warnings) > 0) {
236 session()->flash('warning', implode("\n", $warnings));
239 $draftsEnabled = $this->signedIn;
240 return view('pages/edit', [
242 'book' => $page->book,
244 'draftsEnabled' => $draftsEnabled
249 * Update the specified page in storage.
250 * @param Request $request
251 * @param string $bookSlug
252 * @param string $pageSlug
255 public function update(Request $request, $bookSlug, $pageSlug)
257 $this->validate($request, [
258 'name' => 'required|string|max:255'
260 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
261 $this->checkOwnablePermission('page-update', $page);
262 $this->entityRepo->updatePage($page, $page->book->id, $request->all());
263 Activity::add($page, 'page_update', $page->book->id);
264 return redirect($page->getUrl());
268 * Save a draft update as a revision.
269 * @param Request $request
271 * @return \Illuminate\Http\JsonResponse
273 public function saveDraft(Request $request, $pageId)
275 $page = $this->entityRepo->getById('page', $pageId, true);
276 $this->checkOwnablePermission('page-update', $page);
278 if (!$this->signedIn) {
279 return response()->json([
281 'message' => trans('errors.guests_cannot_save_drafts'),
285 $draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
287 $updateTime = $draft->updated_at->timestamp;
288 return response()->json([
289 'status' => 'success',
290 'message' => trans('entities.pages_edit_draft_save_at'),
291 'timestamp' => $updateTime
296 * Redirect from a special link url which
297 * uses the page id rather than the name.
299 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
301 public function redirectFromLink($pageId)
303 $page = $this->entityRepo->getById('page', $pageId);
304 return redirect($page->getUrl());
308 * Show the deletion page for the specified page.
309 * @param string $bookSlug
310 * @param string $pageSlug
311 * @return \Illuminate\View\View
313 public function showDelete($bookSlug, $pageSlug)
315 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
316 $this->checkOwnablePermission('page-delete', $page);
317 $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
318 return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
323 * Show the deletion page for the specified page.
324 * @param string $bookSlug
326 * @return \Illuminate\View\View
327 * @throws NotFoundException
329 public function showDeleteDraft($bookSlug, $pageId)
331 $page = $this->entityRepo->getById('page', $pageId, true);
332 $this->checkOwnablePermission('page-update', $page);
333 $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
334 return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
338 * Remove the specified page from storage.
339 * @param string $bookSlug
340 * @param string $pageSlug
342 * @internal param int $id
344 public function destroy($bookSlug, $pageSlug)
346 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
348 $this->checkOwnablePermission('page-delete', $page);
349 $this->entityRepo->destroyPage($page);
351 Activity::addMessage('page_delete', $book->id, $page->name);
352 session()->flash('success', trans('entities.pages_delete_success'));
353 return redirect($book->getUrl());
357 * Remove the specified draft page from storage.
358 * @param string $bookSlug
361 * @throws NotFoundException
363 public function destroyDraft($bookSlug, $pageId)
365 $page = $this->entityRepo->getById('page', $pageId, true);
367 $this->checkOwnablePermission('page-update', $page);
368 session()->flash('success', trans('entities.pages_delete_draft_success'));
369 $this->entityRepo->destroyPage($page);
370 return redirect($book->getUrl());
374 * Shows the last revisions for this page.
375 * @param string $bookSlug
376 * @param string $pageSlug
377 * @return \Illuminate\View\View
379 public function showRevisions($bookSlug, $pageSlug)
381 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
382 $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
383 return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
387 * Shows a preview of a single revision
388 * @param string $bookSlug
389 * @param string $pageSlug
390 * @param int $revisionId
391 * @return \Illuminate\View\View
393 public function showRevision($bookSlug, $pageSlug, $revisionId)
395 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
396 $revision = $page->revisions()->where('id', '=', $revisionId)->first();
397 if ($revision === null) {
401 $page->fill($revision->toArray());
402 $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
404 return view('pages/revision', [
406 'book' => $page->book,
407 'revision' => $revision
412 * Shows the changes of a single revision
413 * @param string $bookSlug
414 * @param string $pageSlug
415 * @param int $revisionId
416 * @return \Illuminate\View\View
418 public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
420 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
421 $revision = $page->revisions()->where('id', '=', $revisionId)->first();
422 if ($revision === null) {
426 $prev = $revision->getPrevious();
427 $prevContent = ($prev === null) ? '' : $prev->html;
428 $diff = (new Htmldiff)->diff($prevContent, $revision->html);
430 $page->fill($revision->toArray());
431 $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
433 return view('pages/revision', [
435 'book' => $page->book,
437 'revision' => $revision
442 * Restores a page using the content of the specified revision.
443 * @param string $bookSlug
444 * @param string $pageSlug
445 * @param int $revisionId
446 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
448 public function restoreRevision($bookSlug, $pageSlug, $revisionId)
450 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
451 $this->checkOwnablePermission('page-update', $page);
452 $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
453 Activity::add($page, 'page_restore', $page->book->id);
454 return redirect($page->getUrl());
459 * Deletes a revision using the id of the specified revision.
460 * @param string $bookSlug
461 * @param string $pageSlug
463 * @throws NotFoundException
464 * @throws BadRequestException
465 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
467 public function destroyRevision($bookSlug, $pageSlug, $revId)
469 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
470 $this->checkOwnablePermission('page-delete', $page);
472 $revision = $page->revisions()->where('id', '=', $revId)->first();
473 if ($revision === null) {
474 throw new NotFoundException("Revision #{$revId} not found");
477 // Get the current revision for the page
478 $currentRevision = $page->getCurrentRevision();
480 // Check if its the latest revision, cannot delete latest revision.
481 if (intval($currentRevision->id) === intval($revId)) {
482 session()->flash('error', trans('entities.revision_cannot_delete_latest'));
483 return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
487 session()->flash('success', trans('entities.revision_delete_success'));
488 return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
492 * Exports a page to a PDF.
493 * https://github.com/barryvdh/laravel-dompdf
494 * @param string $bookSlug
495 * @param string $pageSlug
496 * @return \Illuminate\Http\Response
498 public function exportPdf($bookSlug, $pageSlug)
500 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
501 $page->html = $this->entityRepo->renderPage($page);
502 $pdfContent = $this->exportService->pageToPdf($page);
503 return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
507 * Export a page to a self-contained HTML file.
508 * @param string $bookSlug
509 * @param string $pageSlug
510 * @return \Illuminate\Http\Response
512 public function exportHtml($bookSlug, $pageSlug)
514 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
515 $page->html = $this->entityRepo->renderPage($page);
516 $containedHtml = $this->exportService->pageToContainedHtml($page);
517 return $this->downloadResponse($containedHtml, $pageSlug . '.html');
521 * Export a page to a simple plaintext .txt file.
522 * @param string $bookSlug
523 * @param string $pageSlug
524 * @return \Illuminate\Http\Response
526 public function exportPlainText($bookSlug, $pageSlug)
528 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
529 $pageText = $this->exportService->pageToPlainText($page);
530 return $this->downloadResponse($pageText, $pageSlug . '.txt');
534 * Show a listing of recently created pages
535 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
537 public function showRecentlyCreated()
539 $pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
540 return view('pages/detailed-listing', [
541 'title' => trans('entities.recently_created_pages'),
547 * Show a listing of recently created pages
548 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
550 public function showRecentlyUpdated()
552 $pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
553 return view('pages/detailed-listing', [
554 'title' => trans('entities.recently_updated_pages'),
560 * Show the Restrictions view.
561 * @param string $bookSlug
562 * @param string $pageSlug
563 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
565 public function showRestrict($bookSlug, $pageSlug)
567 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
568 $this->checkOwnablePermission('restrictions-manage', $page);
569 $roles = $this->userRepo->getRestrictableRoles();
570 return view('pages/restrictions', [
577 * Show the view to choose a new parent to move a page into.
578 * @param string $bookSlug
579 * @param string $pageSlug
581 * @throws NotFoundException
583 public function showMove($bookSlug, $pageSlug)
585 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
586 $this->checkOwnablePermission('page-update', $page);
587 return view('pages/move', [
588 'book' => $page->book,
594 * Does the action of moving the location of a page
595 * @param string $bookSlug
596 * @param string $pageSlug
597 * @param Request $request
599 * @throws NotFoundException
601 public function move($bookSlug, $pageSlug, Request $request)
603 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
604 $this->checkOwnablePermission('page-update', $page);
606 $entitySelection = $request->get('entity_selection', null);
607 if ($entitySelection === null || $entitySelection === '') {
608 return redirect($page->getUrl());
611 $stringExploded = explode(':', $entitySelection);
612 $entityType = $stringExploded[0];
613 $entityId = intval($stringExploded[1]);
617 $parent = $this->entityRepo->getById($entityType, $entityId);
618 } catch (\Exception $e) {
619 session()->flash(trans('entities.selected_book_chapter_not_found'));
620 return redirect()->back();
623 $this->checkOwnablePermission('page-create', $parent);
625 $this->entityRepo->changePageParent($page, $parent);
626 Activity::add($page, 'page_move', $page->book->id);
627 session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
629 return redirect($page->getUrl());
633 * Show the view to copy a page.
634 * @param string $bookSlug
635 * @param string $pageSlug
637 * @throws NotFoundException
639 public function showCopy($bookSlug, $pageSlug)
641 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
642 $this->checkOwnablePermission('page-update', $page);
643 session()->flashInput(['name' => $page->name]);
644 return view('pages/copy', [
645 'book' => $page->book,
651 * Create a copy of a page within the requested target destination.
652 * @param string $bookSlug
653 * @param string $pageSlug
654 * @param Request $request
656 * @throws NotFoundException
658 public function copy($bookSlug, $pageSlug, Request $request)
660 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
661 $this->checkOwnablePermission('page-update', $page);
663 $entitySelection = $request->get('entity_selection', null);
664 if ($entitySelection === null || $entitySelection === '') {
665 $parent = $page->chapter ? $page->chapter : $page->book;
667 $stringExploded = explode(':', $entitySelection);
668 $entityType = $stringExploded[0];
669 $entityId = intval($stringExploded[1]);
672 $parent = $this->entityRepo->getById($entityType, $entityId);
673 } catch (\Exception $e) {
674 session()->flash(trans('entities.selected_book_chapter_not_found'));
675 return redirect()->back();
679 $this->checkOwnablePermission('page-create', $parent);
681 $pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
683 Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
684 session()->flash('success', trans('entities.pages_copy_success'));
686 return redirect($pageCopy->getUrl());
690 * Set the permissions for this page.
691 * @param string $bookSlug
692 * @param string $pageSlug
693 * @param Request $request
694 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
695 * @throws NotFoundException
697 public function restrict($bookSlug, $pageSlug, Request $request)
699 $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
700 $this->checkOwnablePermission('restrictions-manage', $page);
701 $this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
702 session()->flash('success', trans('entities.pages_permissions_success'));
703 return redirect($page->getUrl());