]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/PageController.php
Refactored the code to first check for the permissions before sorting the book.
[bookstack] / app / Http / Controllers / PageController.php
1 <?php namespace BookStack\Http\Controllers;
2
3 use Activity;
4 use BookStack\Exceptions\NotFoundException;
5 use BookStack\Repos\EntityRepo;
6 use BookStack\Repos\UserRepo;
7 use BookStack\Services\ExportService;
8 use Carbon\Carbon;
9 use Illuminate\Http\Request;
10 use Illuminate\Http\Response;
11 use Views;
12 use GatherContent\Htmldiff\Htmldiff;
13
14 class PageController extends Controller
15 {
16
17     protected $entityRepo;
18     protected $exportService;
19     protected $userRepo;
20
21     /**
22      * PageController constructor.
23      * @param EntityRepo $entityRepo
24      * @param ExportService $exportService
25      * @param UserRepo $userRepo
26      */
27     public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
28     {
29         $this->entityRepo = $entityRepo;
30         $this->exportService = $exportService;
31         $this->userRepo = $userRepo;
32         parent::__construct();
33     }
34
35     /**
36      * Show the form for creating a new page.
37      * @param string $bookSlug
38      * @param string $chapterSlug
39      * @return Response
40      * @internal param bool $pageSlug
41      */
42     public function create($bookSlug, $chapterSlug = null)
43     {
44         $book = $this->entityRepo->getBySlug('book', $bookSlug);
45         $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
46         $parent = $chapter ? $chapter : $book;
47         $this->checkOwnablePermission('page-create', $parent);
48
49         // Redirect to draft edit screen if signed in
50         if ($this->signedIn) {
51             $draft = $this->entityRepo->getDraftPage($book, $chapter);
52             return redirect($draft->getUrl());
53         }
54
55         // Otherwise show edit view
56         $this->setPageTitle(trans('entities.pages_new'));
57         return view('pages/guest-create', ['parent' => $parent]);
58     }
59
60     /**
61      * Create a new page as a guest user.
62      * @param Request $request
63      * @param string $bookSlug
64      * @param string|null $chapterSlug
65      * @return mixed
66      * @throws NotFoundException
67      */
68     public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
69     {
70         $this->validate($request, [
71             'name' => 'required|string|max:255'
72         ]);
73
74         $book = $this->entityRepo->getBySlug('book', $bookSlug);
75         $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
76         $parent = $chapter ? $chapter : $book;
77         $this->checkOwnablePermission('page-create', $parent);
78
79         $page = $this->entityRepo->getDraftPage($book, $chapter);
80         $this->entityRepo->publishPageDraft($page, [
81             'name' => $request->get('name'),
82             'html' => ''
83         ]);
84         return redirect($page->getUrl('/edit'));
85     }
86
87     /**
88      * Show form to continue editing a draft page.
89      * @param string $bookSlug
90      * @param int $pageId
91      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
92      */
93     public function editDraft($bookSlug, $pageId)
94     {
95         $draft = $this->entityRepo->getById('page', $pageId, true);
96         $this->checkOwnablePermission('page-create', $draft->book);
97         $this->setPageTitle(trans('entities.pages_edit_draft'));
98
99         $draftsEnabled = $this->signedIn;
100         return view('pages/edit', [
101             'page' => $draft,
102             'book' => $draft->book,
103             'isDraft' => true,
104             'draftsEnabled' => $draftsEnabled
105         ]);
106     }
107
108     /**
109      * Store a new page by changing a draft into a page.
110      * @param  Request $request
111      * @param  string $bookSlug
112      * @param  int $pageId
113      * @return Response
114      */
115     public function store(Request $request, $bookSlug, $pageId)
116     {
117         $this->validate($request, [
118             'name' => 'required|string|max:255'
119         ]);
120
121         $input = $request->all();
122         $book = $this->entityRepo->getBySlug('book', $bookSlug);
123
124         $draftPage = $this->entityRepo->getById('page', $pageId, true);
125
126         $chapterId = intval($draftPage->chapter_id);
127         $parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
128         $this->checkOwnablePermission('page-create', $parent);
129
130         if ($parent->isA('chapter')) {
131             $input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
132         } else {
133             $input['priority'] = $this->entityRepo->getNewBookPriority($parent);
134         }
135
136         $page = $this->entityRepo->publishPageDraft($draftPage, $input);
137
138         Activity::add($page, 'page_create', $book->id);
139         return redirect($page->getUrl());
140     }
141
142     /**
143      * Display the specified page.
144      * If the page is not found via the slug the revisions are searched for a match.
145      * @param string $bookSlug
146      * @param string $pageSlug
147      * @return Response
148      * @throws NotFoundException
149      */
150     public function show($bookSlug, $pageSlug)
151     {
152         try {
153             $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
154         } catch (NotFoundException $e) {
155             $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
156             if ($page === null) throw $e;
157             return redirect($page->getUrl());
158         }
159
160         $this->checkOwnablePermission('page-view', $page);
161
162         $page->html = $this->entityRepo->renderPage($page);
163         $sidebarTree = $this->entityRepo->getBookChildren($page->book);
164         $pageNav = $this->entityRepo->getPageNav($page->html);
165
166         // check if the comment's are enabled
167         $commentsEnabled = !setting('app-disable-comments');
168         if ($commentsEnabled) {
169             $page->load(['comments.createdBy']);
170         }
171
172         Views::add($page);
173         $this->setPageTitle($page->getShortName());
174         return view('pages/show', [
175             'page' => $page,'book' => $page->book,
176             'current' => $page,
177             'sidebarTree' => $sidebarTree,
178             'commentsEnabled' => $commentsEnabled,
179             'pageNav' => $pageNav
180         ]);
181     }
182
183     /**
184      * Get page from an ajax request.
185      * @param int $pageId
186      * @return \Illuminate\Http\JsonResponse
187      */
188     public function getPageAjax($pageId)
189     {
190         $page = $this->entityRepo->getById('page', $pageId);
191         return response()->json($page);
192     }
193
194     /**
195      * Show the form for editing the specified page.
196      * @param string $bookSlug
197      * @param string $pageSlug
198      * @return Response
199      */
200     public function edit($bookSlug, $pageSlug)
201     {
202         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
203         $this->checkOwnablePermission('page-update', $page);
204         $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
205         $page->isDraft = false;
206
207         // Check for active editing
208         $warnings = [];
209         if ($this->entityRepo->isPageEditingActive($page, 60)) {
210             $warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
211         }
212
213         // Check for a current draft version for this user
214         if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
215             $draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
216             $page->name = $draft->name;
217             $page->html = $draft->html;
218             $page->markdown = $draft->markdown;
219             $page->isDraft = true;
220             $warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
221         }
222
223         if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
224
225         $draftsEnabled = $this->signedIn;
226         return view('pages/edit', [
227             'page' => $page,
228             'book' => $page->book,
229             'current' => $page,
230             'draftsEnabled' => $draftsEnabled
231         ]);
232     }
233
234     /**
235      * Update the specified page in storage.
236      * @param  Request $request
237      * @param  string $bookSlug
238      * @param  string $pageSlug
239      * @return Response
240      */
241     public function update(Request $request, $bookSlug, $pageSlug)
242     {
243         $this->validate($request, [
244             'name' => 'required|string|max:255'
245         ]);
246         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
247         $this->checkOwnablePermission('page-update', $page);
248         $this->entityRepo->updatePage($page, $page->book->id, $request->all());
249         Activity::add($page, 'page_update', $page->book->id);
250         return redirect($page->getUrl());
251     }
252
253     /**
254      * Save a draft update as a revision.
255      * @param Request $request
256      * @param int $pageId
257      * @return \Illuminate\Http\JsonResponse
258      */
259     public function saveDraft(Request $request, $pageId)
260     {
261         $page = $this->entityRepo->getById('page', $pageId, true);
262         $this->checkOwnablePermission('page-update', $page);
263
264         if (!$this->signedIn) {
265             return response()->json([
266                 'status' => 'error',
267                 'message' => trans('errors.guests_cannot_save_drafts'),
268             ], 500);
269         }
270
271         $draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
272
273         $updateTime = $draft->updated_at->timestamp;
274         $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
275         return response()->json([
276             'status'    => 'success',
277             'message'   => trans('entities.pages_edit_draft_save_at'),
278             'timestamp' => $utcUpdateTimestamp
279         ]);
280     }
281
282     /**
283      * Redirect from a special link url which
284      * uses the page id rather than the name.
285      * @param int $pageId
286      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
287      */
288     public function redirectFromLink($pageId)
289     {
290         $page = $this->entityRepo->getById('page', $pageId);
291         return redirect($page->getUrl());
292     }
293
294     /**
295      * Show the deletion page for the specified page.
296      * @param string $bookSlug
297      * @param string $pageSlug
298      * @return \Illuminate\View\View
299      */
300     public function showDelete($bookSlug, $pageSlug)
301     {
302         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
303         $this->checkOwnablePermission('page-delete', $page);
304         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
305         return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
306     }
307
308
309     /**
310      * Show the deletion page for the specified page.
311      * @param string $bookSlug
312      * @param int $pageId
313      * @return \Illuminate\View\View
314      * @throws NotFoundException
315      */
316     public function showDeleteDraft($bookSlug, $pageId)
317     {
318         $page = $this->entityRepo->getById('page', $pageId, true);
319         $this->checkOwnablePermission('page-update', $page);
320         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
321         return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
322     }
323
324     /**
325      * Remove the specified page from storage.
326      * @param string $bookSlug
327      * @param string $pageSlug
328      * @return Response
329      * @internal param int $id
330      */
331     public function destroy($bookSlug, $pageSlug)
332     {
333         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
334         $book = $page->book;
335         $this->checkOwnablePermission('page-delete', $page);
336         $this->entityRepo->destroyPage($page);
337
338         Activity::addMessage('page_delete', $book->id, $page->name);
339         session()->flash('success', trans('entities.pages_delete_success'));
340         return redirect($book->getUrl());
341     }
342
343     /**
344      * Remove the specified draft page from storage.
345      * @param string $bookSlug
346      * @param int $pageId
347      * @return Response
348      * @throws NotFoundException
349      */
350     public function destroyDraft($bookSlug, $pageId)
351     {
352         $page = $this->entityRepo->getById('page', $pageId, true);
353         $book = $page->book;
354         $this->checkOwnablePermission('page-update', $page);
355         session()->flash('success', trans('entities.pages_delete_draft_success'));
356         $this->entityRepo->destroyPage($page);
357         return redirect($book->getUrl());
358     }
359
360     /**
361      * Shows the last revisions for this page.
362      * @param string $bookSlug
363      * @param string $pageSlug
364      * @return \Illuminate\View\View
365      */
366     public function showRevisions($bookSlug, $pageSlug)
367     {
368         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
369         $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
370         return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
371     }
372
373     /**
374      * Shows a preview of a single revision
375      * @param string $bookSlug
376      * @param string $pageSlug
377      * @param int $revisionId
378      * @return \Illuminate\View\View
379      */
380     public function showRevision($bookSlug, $pageSlug, $revisionId)
381     {
382         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
383         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
384         if ($revision === null) {
385             abort(404);
386         }
387
388         $page->fill($revision->toArray());
389         $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
390
391         return view('pages/revision', [
392             'page' => $page,
393             'book' => $page->book,
394             'revision' => $revision
395         ]);
396     }
397
398     /**
399      * Shows the changes of a single revision
400      * @param string $bookSlug
401      * @param string $pageSlug
402      * @param int $revisionId
403      * @return \Illuminate\View\View
404      */
405     public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
406     {
407         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
408         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
409         if ($revision === null) {
410             abort(404);
411         }
412
413         $prev = $revision->getPrevious();
414         $prevContent = ($prev === null) ? '' : $prev->html;
415         $diff = (new Htmldiff)->diff($prevContent, $revision->html);
416
417         $page->fill($revision->toArray());
418         $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
419
420         return view('pages/revision', [
421             'page' => $page,
422             'book' => $page->book,
423             'diff' => $diff,
424             'revision' => $revision
425         ]);
426     }
427
428     /**
429      * Restores a page using the content of the specified revision.
430      * @param string $bookSlug
431      * @param string $pageSlug
432      * @param int $revisionId
433      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
434      */
435     public function restoreRevision($bookSlug, $pageSlug, $revisionId)
436     {
437         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
438         $this->checkOwnablePermission('page-update', $page);
439         $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
440         Activity::add($page, 'page_restore', $page->book->id);
441         return redirect($page->getUrl());
442     }
443
444     /**
445      * Exports a page to a PDF.
446      * https://github.com/barryvdh/laravel-dompdf
447      * @param string $bookSlug
448      * @param string $pageSlug
449      * @return \Illuminate\Http\Response
450      */
451     public function exportPdf($bookSlug, $pageSlug)
452     {
453         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
454         $page->html = $this->entityRepo->renderPage($page);
455         $pdfContent = $this->exportService->pageToPdf($page);
456         return response()->make($pdfContent, 200, [
457             'Content-Type'        => 'application/octet-stream',
458             'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
459         ]);
460     }
461
462     /**
463      * Export a page to a self-contained HTML file.
464      * @param string $bookSlug
465      * @param string $pageSlug
466      * @return \Illuminate\Http\Response
467      */
468     public function exportHtml($bookSlug, $pageSlug)
469     {
470         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
471         $page->html = $this->entityRepo->renderPage($page);
472         $containedHtml = $this->exportService->pageToContainedHtml($page);
473         return response()->make($containedHtml, 200, [
474             'Content-Type'        => 'application/octet-stream',
475             'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
476         ]);
477     }
478
479     /**
480      * Export a page to a simple plaintext .txt file.
481      * @param string $bookSlug
482      * @param string $pageSlug
483      * @return \Illuminate\Http\Response
484      */
485     public function exportPlainText($bookSlug, $pageSlug)
486     {
487         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
488         $containedHtml = $this->exportService->pageToPlainText($page);
489         return response()->make($containedHtml, 200, [
490             'Content-Type'        => 'application/octet-stream',
491             'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
492         ]);
493     }
494
495     /**
496      * Show a listing of recently created pages
497      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
498      */
499     public function showRecentlyCreated()
500     {
501         $pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
502         return view('pages/detailed-listing', [
503             'title' => trans('entities.recently_created_pages'),
504             'pages' => $pages
505         ]);
506     }
507
508     /**
509      * Show a listing of recently created pages
510      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
511      */
512     public function showRecentlyUpdated()
513     {
514         $pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
515         return view('pages/detailed-listing', [
516             'title' => trans('entities.recently_updated_pages'),
517             'pages' => $pages
518         ]);
519     }
520
521     /**
522      * Show the Restrictions view.
523      * @param string $bookSlug
524      * @param string $pageSlug
525      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
526      */
527     public function showRestrict($bookSlug, $pageSlug)
528     {
529         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
530         $this->checkOwnablePermission('restrictions-manage', $page);
531         $roles = $this->userRepo->getRestrictableRoles();
532         return view('pages/restrictions', [
533             'page'  => $page,
534             'roles' => $roles
535         ]);
536     }
537
538     /**
539      * Show the view to choose a new parent to move a page into.
540      * @param string $bookSlug
541      * @param string $pageSlug
542      * @return mixed
543      * @throws NotFoundException
544      */
545     public function showMove($bookSlug, $pageSlug)
546     {
547         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
548         $this->checkOwnablePermission('page-update', $page);
549         return view('pages/move', [
550             'book' => $page->book,
551             'page' => $page
552         ]);
553     }
554
555     /**
556      * Does the action of moving the location of a page
557      * @param string $bookSlug
558      * @param string $pageSlug
559      * @param Request $request
560      * @return mixed
561      * @throws NotFoundException
562      */
563     public function move($bookSlug, $pageSlug, Request $request)
564     {
565         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
566         $this->checkOwnablePermission('page-update', $page);
567
568         $entitySelection = $request->get('entity_selection', null);
569         if ($entitySelection === null || $entitySelection === '') {
570             return redirect($page->getUrl());
571         }
572
573         $stringExploded = explode(':', $entitySelection);
574         $entityType = $stringExploded[0];
575         $entityId = intval($stringExploded[1]);
576
577
578         try {
579             $parent = $this->entityRepo->getById($entityType, $entityId);
580         } catch (\Exception $e) {
581             session()->flash(trans('entities.selected_book_chapter_not_found'));
582             return redirect()->back();
583         }
584
585         $this->entityRepo->changePageParent($page, $parent);
586         Activity::add($page, 'page_move', $page->book->id);
587         session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
588
589         return redirect($page->getUrl());
590     }
591
592     /**
593      * Set the permissions for this page.
594      * @param string $bookSlug
595      * @param string $pageSlug
596      * @param Request $request
597      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
598      */
599     public function restrict($bookSlug, $pageSlug, Request $request)
600     {
601         $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
602         $this->checkOwnablePermission('restrictions-manage', $page);
603         $this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
604         session()->flash('success', trans('entities.pages_permissions_success'));
605         return redirect($page->getUrl());
606     }
607
608 }