1 <?php namespace BookStack\Http\Controllers;
4 use BookStack\Exceptions\NotFoundException;
5 use BookStack\Repos\UserRepo;
6 use BookStack\Services\ExportService;
8 use Illuminate\Http\Request;
9 use BookStack\Http\Requests;
10 use BookStack\Repos\BookRepo;
11 use BookStack\Repos\ChapterRepo;
12 use BookStack\Repos\PageRepo;
13 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
15 use GatherContent\Htmldiff\Htmldiff;
17 class PageController extends Controller
22 protected $chapterRepo;
23 protected $exportService;
27 * PageController constructor.
28 * @param PageRepo $pageRepo
29 * @param BookRepo $bookRepo
30 * @param ChapterRepo $chapterRepo
31 * @param ExportService $exportService
32 * @param UserRepo $userRepo
34 public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
36 $this->pageRepo = $pageRepo;
37 $this->bookRepo = $bookRepo;
38 $this->chapterRepo = $chapterRepo;
39 $this->exportService = $exportService;
40 $this->userRepo = $userRepo;
41 parent::__construct();
45 * Show the form for creating a new page.
46 * @param string $bookSlug
47 * @param string $chapterSlug
49 * @internal param bool $pageSlug
51 public function create($bookSlug, $chapterSlug = null)
53 $book = $this->bookRepo->getBySlug($bookSlug);
54 $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
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 edit view
65 $this->setPageTitle('Create New Page');
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 $book = $this->bookRepo->getBySlug($bookSlug);
84 $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
85 $parent = $chapter ? $chapter : $book;
86 $this->checkOwnablePermission('page-create', $parent);
88 $page = $this->pageRepo->getDraftPage($book, $chapter);
89 $this->pageRepo->publishDraft($page, [
90 'name' => $request->get('name'),
93 return redirect($page->getUrl('/edit'));
97 * Show form to continue editing a draft page.
98 * @param string $bookSlug
100 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
102 public function editDraft($bookSlug, $pageId)
104 $book = $this->bookRepo->getBySlug($bookSlug);
105 $draft = $this->pageRepo->getById($pageId, true);
106 $this->checkOwnablePermission('page-create', $book);
107 $this->setPageTitle('Edit Page Draft');
109 return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
113 * Store a new page by changing a draft into a page.
114 * @param Request $request
115 * @param string $bookSlug
118 public function store(Request $request, $bookSlug, $pageId)
120 $this->validate($request, [
121 'name' => 'required|string|max:255'
124 $input = $request->all();
125 $book = $this->bookRepo->getBySlug($bookSlug);
127 $draftPage = $this->pageRepo->getById($pageId, true);
129 $chapterId = intval($draftPage->chapter_id);
130 $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
131 $this->checkOwnablePermission('page-create', $parent);
133 if ($parent->isA('chapter')) {
134 $input['priority'] = $this->chapterRepo->getNewPriority($parent);
136 $input['priority'] = $this->bookRepo->getNewPriority($parent);
139 $page = $this->pageRepo->publishDraft($draftPage, $input);
141 Activity::add($page, 'page_create', $book->id);
142 return redirect($page->getUrl());
146 * Display the specified page.
147 * If the page is not found via the slug the
148 * revisions are searched for a match.
149 * @param string $bookSlug
150 * @param string $pageSlug
153 public function show($bookSlug, $pageSlug)
155 $book = $this->bookRepo->getBySlug($bookSlug);
158 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
159 } catch (NotFoundException $e) {
160 $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
161 if ($page === null) abort(404);
162 return redirect($page->getUrl());
165 $this->checkOwnablePermission('page-view', $page);
167 $sidebarTree = $this->bookRepo->getChildren($book);
168 $pageNav = $this->pageRepo->getPageNav($page);
171 $this->setPageTitle($page->getShortName());
172 return view('pages/show', ['page' => $page, 'book' => $book,
173 'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]);
177 * Get page from an ajax request.
179 * @return \Illuminate\Http\JsonResponse
181 public function getPageAjax($pageId)
183 $page = $this->pageRepo->getById($pageId);
184 return response()->json($page);
188 * Show the form for editing the specified page.
189 * @param string $bookSlug
190 * @param string $pageSlug
193 public function edit($bookSlug, $pageSlug)
195 $book = $this->bookRepo->getBySlug($bookSlug);
196 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
197 $this->checkOwnablePermission('page-update', $page);
198 $this->setPageTitle('Editing Page ' . $page->getShortName());
199 $page->isDraft = false;
201 // Check for active editing
203 if ($this->pageRepo->isPageEditingActive($page, 60)) {
204 $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
207 // Check for a current draft version for this user
208 if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
209 $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
210 $page->name = $draft->name;
211 $page->html = $draft->html;
212 $page->markdown = $draft->markdown;
213 $page->isDraft = true;
214 $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
217 if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
219 $draftsEnabled = $this->signedIn;
220 return view('pages/edit', [
224 'draftsEnabled' => $draftsEnabled
229 * Update the specified page in storage.
230 * @param Request $request
231 * @param string $bookSlug
232 * @param string $pageSlug
235 public function update(Request $request, $bookSlug, $pageSlug)
237 $this->validate($request, [
238 'name' => 'required|string|max:255'
240 $book = $this->bookRepo->getBySlug($bookSlug);
241 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
242 $this->checkOwnablePermission('page-update', $page);
243 $this->pageRepo->updatePage($page, $book->id, $request->all());
244 Activity::add($page, 'page_update', $book->id);
245 return redirect($page->getUrl());
249 * Save a draft update as a revision.
250 * @param Request $request
252 * @return \Illuminate\Http\JsonResponse
254 public function saveDraft(Request $request, $pageId)
256 $page = $this->pageRepo->getById($pageId, true);
257 $this->checkOwnablePermission('page-update', $page);
259 if (!$this->signedIn) {
260 return response()->json([
262 'message' => 'Guests cannot save drafts',
267 $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
269 $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
272 $updateTime = $draft->updated_at->timestamp;
273 $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
274 return response()->json([
275 'status' => 'success',
276 'message' => 'Draft saved at ',
277 'timestamp' => $utcUpdateTimestamp
282 * Redirect from a special link url which
283 * uses the page id rather than the name.
285 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
287 public function redirectFromLink($pageId)
289 $page = $this->pageRepo->getById($pageId);
290 return redirect($page->getUrl());
294 * Show the deletion page for the specified page.
295 * @param string $bookSlug
296 * @param string $pageSlug
297 * @return \Illuminate\View\View
299 public function showDelete($bookSlug, $pageSlug)
301 $book = $this->bookRepo->getBySlug($bookSlug);
302 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
303 $this->checkOwnablePermission('page-delete', $page);
304 $this->setPageTitle('Delete Page ' . $page->getShortName());
305 return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
310 * Show the deletion page for the specified page.
311 * @param string $bookSlug
313 * @return \Illuminate\View\View
314 * @throws NotFoundException
316 public function showDeleteDraft($bookSlug, $pageId)
318 $book = $this->bookRepo->getBySlug($bookSlug);
319 $page = $this->pageRepo->getById($pageId, true);
320 $this->checkOwnablePermission('page-update', $page);
321 $this->setPageTitle('Delete Draft Page ' . $page->getShortName());
322 return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
326 * Remove the specified page from storage.
327 * @param string $bookSlug
328 * @param string $pageSlug
330 * @internal param int $id
332 public function destroy($bookSlug, $pageSlug)
334 $book = $this->bookRepo->getBySlug($bookSlug);
335 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
336 $this->checkOwnablePermission('page-delete', $page);
337 Activity::addMessage('page_delete', $book->id, $page->name);
338 session()->flash('success', 'Page deleted');
339 $this->pageRepo->destroy($page);
340 return redirect($book->getUrl());
344 * Remove the specified draft page from storage.
345 * @param string $bookSlug
348 * @throws NotFoundException
350 public function destroyDraft($bookSlug, $pageId)
352 $book = $this->bookRepo->getBySlug($bookSlug);
353 $page = $this->pageRepo->getById($pageId, true);
354 $this->checkOwnablePermission('page-update', $page);
355 session()->flash('success', 'Draft deleted');
356 $this->pageRepo->destroy($page);
357 return redirect($book->getUrl());
361 * Shows the last revisions for this page.
362 * @param string $bookSlug
363 * @param string $pageSlug
364 * @return \Illuminate\View\View
366 public function showRevisions($bookSlug, $pageSlug)
368 $book = $this->bookRepo->getBySlug($bookSlug);
369 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
370 $this->setPageTitle('Revisions For ' . $page->getShortName());
371 return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
375 * Shows a preview of a single revision
376 * @param string $bookSlug
377 * @param string $pageSlug
378 * @param int $revisionId
379 * @return \Illuminate\View\View
381 public function showRevision($bookSlug, $pageSlug, $revisionId)
383 $book = $this->bookRepo->getBySlug($bookSlug);
384 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
385 $revision = $this->pageRepo->getRevisionById($revisionId);
387 $page->fill($revision->toArray());
388 $this->setPageTitle('Page Revision For ' . $page->getShortName());
390 return view('pages/revision', [
397 * Shows the changes of a single revision
398 * @param string $bookSlug
399 * @param string $pageSlug
400 * @param int $revisionId
401 * @return \Illuminate\View\View
403 public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
405 $book = $this->bookRepo->getBySlug($bookSlug);
406 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
407 $revision = $this->pageRepo->getRevisionById($revisionId);
409 $prev = $revision->getPrevious();
410 $prevContent = ($prev === null) ? '' : $prev->html;
411 $diff = (new Htmldiff)->diff($prevContent, $revision->html);
413 $page->fill($revision->toArray());
414 $this->setPageTitle('Page Revision For ' . $page->getShortName());
416 return view('pages/revision', [
424 * Restores a page using the content of the specified revision.
425 * @param string $bookSlug
426 * @param string $pageSlug
427 * @param int $revisionId
428 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
430 public function restoreRevision($bookSlug, $pageSlug, $revisionId)
432 $book = $this->bookRepo->getBySlug($bookSlug);
433 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
434 $this->checkOwnablePermission('page-update', $page);
435 $page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
436 Activity::add($page, 'page_restore', $book->id);
437 return redirect($page->getUrl());
441 * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
442 * https://github.com/barryvdh/laravel-dompdf
443 * @param string $bookSlug
444 * @param string $pageSlug
445 * @return \Illuminate\Http\Response
447 public function exportPdf($bookSlug, $pageSlug)
449 $book = $this->bookRepo->getBySlug($bookSlug);
450 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
451 $pdfContent = $this->exportService->pageToPdf($page);
452 return response()->make($pdfContent, 200, [
453 'Content-Type' => 'application/octet-stream',
454 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
459 * Export a page to a self-contained HTML file.
460 * @param string $bookSlug
461 * @param string $pageSlug
462 * @return \Illuminate\Http\Response
464 public function exportHtml($bookSlug, $pageSlug)
466 $book = $this->bookRepo->getBySlug($bookSlug);
467 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
468 $containedHtml = $this->exportService->pageToContainedHtml($page);
469 return response()->make($containedHtml, 200, [
470 'Content-Type' => 'application/octet-stream',
471 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
476 * Export a page to a simple plaintext .txt file.
477 * @param string $bookSlug
478 * @param string $pageSlug
479 * @return \Illuminate\Http\Response
481 public function exportPlainText($bookSlug, $pageSlug)
483 $book = $this->bookRepo->getBySlug($bookSlug);
484 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
485 $containedHtml = $this->exportService->pageToPlainText($page);
486 return response()->make($containedHtml, 200, [
487 'Content-Type' => 'application/octet-stream',
488 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
493 * Show a listing of recently created pages
494 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
496 public function showRecentlyCreated()
498 $pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created'));
499 return view('pages/detailed-listing', [
500 'title' => 'Recently Created Pages',
506 * Show a listing of recently created pages
507 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
509 public function showRecentlyUpdated()
511 $pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated'));
512 return view('pages/detailed-listing', [
513 'title' => 'Recently Updated Pages',
519 * Show the Restrictions view.
520 * @param string $bookSlug
521 * @param string $pageSlug
522 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
524 public function showRestrict($bookSlug, $pageSlug)
526 $book = $this->bookRepo->getBySlug($bookSlug);
527 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
528 $this->checkOwnablePermission('restrictions-manage', $page);
529 $roles = $this->userRepo->getRestrictableRoles();
530 return view('pages/restrictions', [
537 * Show the view to choose a new parent to move a page into.
538 * @param string $bookSlug
539 * @param string $pageSlug
541 * @throws NotFoundException
543 public function showMove($bookSlug, $pageSlug)
545 $book = $this->bookRepo->getBySlug($bookSlug);
546 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
547 $this->checkOwnablePermission('page-update', $page);
548 return view('pages/move', [
555 * Does the action of moving the location of a page
556 * @param string $bookSlug
557 * @param string $pageSlug
558 * @param Request $request
560 * @throws NotFoundException
562 public function move($bookSlug, $pageSlug, Request $request)
564 $book = $this->bookRepo->getBySlug($bookSlug);
565 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
566 $this->checkOwnablePermission('page-update', $page);
568 $entitySelection = $request->get('entity_selection', null);
569 if ($entitySelection === null || $entitySelection === '') {
570 return redirect($page->getUrl());
573 $stringExploded = explode(':', $entitySelection);
574 $entityType = $stringExploded[0];
575 $entityId = intval($stringExploded[1]);
579 if ($entityType == 'chapter') {
580 $parent = $this->chapterRepo->getById($entityId);
581 } else if ($entityType == 'book') {
582 $parent = $this->bookRepo->getById($entityId);
585 if ($parent === false || $parent === null) {
586 session()->flash('The selected Book or Chapter was not found');
587 return redirect()->back();
590 $this->pageRepo->changePageParent($page, $parent);
591 Activity::add($page, 'page_move', $page->book->id);
592 session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
594 return redirect($page->getUrl());
598 * Set the permissions for this page.
599 * @param string $bookSlug
600 * @param string $pageSlug
601 * @param Request $request
602 * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
604 public function restrict($bookSlug, $pageSlug, Request $request)
606 $book = $this->bookRepo->getBySlug($bookSlug);
607 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
608 $this->checkOwnablePermission('restrictions-manage', $page);
609 $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
610 session()->flash('success', 'Page Permissions Updated');
611 return redirect($page->getUrl());