]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/PageController.php
Refactored notification showing and global view data
[bookstack] / app / Http / Controllers / PageController.php
1 <?php namespace BookStack\Http\Controllers;
2
3 use Activity;
4 use BookStack\Auth\UserRepo;
5 use BookStack\Entities\Repos\PageRepo;
6 use BookStack\Exceptions\NotFoundException;
7 use Exception;
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;
16 use Throwable;
17 use Views;
18
19 class PageController extends Controller
20 {
21
22     protected $pageRepo;
23     protected $userRepo;
24
25     /**
26      * PageController constructor.
27      * @param PageRepo $pageRepo
28      * @param UserRepo $userRepo
29      */
30     public function __construct(PageRepo $pageRepo, UserRepo $userRepo)
31     {
32         $this->pageRepo = $pageRepo;
33         $this->userRepo = $userRepo;
34         parent::__construct();
35     }
36
37     /**
38      * Show the form for creating a new page.
39      * @param string $bookSlug
40      * @param string $chapterSlug
41      * @return Response
42      * @internal param bool $pageSlug
43      * @throws NotFoundException
44      */
45     public function create($bookSlug, $chapterSlug = null)
46     {
47         if ($chapterSlug !== null) {
48             $chapter = $this->pageRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
49             $book = $chapter->book;
50         } else {
51             $chapter = null;
52             $book = $this->pageRepo->getEntityBySlug('book', $bookSlug);
53         }
54
55         $parent = $chapter ? $chapter : $book;
56         $this->checkOwnablePermission('page-create', $parent);
57
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());
62         }
63
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]);
67     }
68
69     /**
70      * Create a new page as a guest user.
71      * @param Request $request
72      * @param string $bookSlug
73      * @param string|null $chapterSlug
74      * @return mixed
75      * @throws NotFoundException
76      */
77     public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
78     {
79         $this->validate($request, [
80             'name' => 'required|string|max:255'
81         ]);
82
83         if ($chapterSlug !== null) {
84             $chapter = $this->pageRepo->getEntityBySlug('chapter', $chapterSlug, $bookSlug);
85             $book = $chapter->book;
86         } else {
87             $chapter = null;
88             $book = $this->pageRepo->getEntityBySlug('book', $bookSlug);
89         }
90
91         $parent = $chapter ? $chapter : $book;
92         $this->checkOwnablePermission('page-create', $parent);
93
94         $page = $this->pageRepo->getDraftPage($book, $chapter);
95         $this->pageRepo->publishPageDraft($page, [
96             'name' => $request->get('name'),
97             'html' => ''
98         ]);
99         return redirect($page->getUrl('/edit'));
100     }
101
102     /**
103      * Show form to continue editing a draft page.
104      * @param string $bookSlug
105      * @param int $pageId
106      * @return Factory|View
107      */
108     public function editDraft($bookSlug, $pageId)
109     {
110         $draft = $this->pageRepo->getById('page', $pageId, true);
111         $this->checkOwnablePermission('page-create', $draft->parent);
112         $this->setPageTitle(trans('entities.pages_edit_draft'));
113
114         $draftsEnabled = $this->signedIn;
115         $templates = $this->pageRepo->getPageTemplates(10);
116
117         return view('pages.edit', [
118             'page' => $draft,
119             'book' => $draft->book,
120             'isDraft' => true,
121             'draftsEnabled' => $draftsEnabled,
122             'templates' => $templates,
123         ]);
124     }
125
126     /**
127      * Store a new page by changing a draft into a page.
128      * @param  Request $request
129      * @param  string $bookSlug
130      * @param  int $pageId
131      * @return Response
132      */
133     public function store(Request $request, $bookSlug, $pageId)
134     {
135         $this->validate($request, [
136             'name' => 'required|string|max:255'
137         ]);
138
139         $input = $request->all();
140         $draftPage = $this->pageRepo->getById('page', $pageId, true);
141         $book = $draftPage->book;
142
143         $parent = $draftPage->parent;
144         $this->checkOwnablePermission('page-create', $parent);
145
146         if ($parent->isA('chapter')) {
147             $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
148         } else {
149             $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
150         }
151
152         $page = $this->pageRepo->publishPageDraft($draftPage, $input);
153
154         Activity::add($page, 'page_create', $book->id);
155         return redirect($page->getUrl());
156     }
157
158     /**
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
163      * @return Response
164      * @throws NotFoundException
165      */
166     public function show($bookSlug, $pageSlug)
167     {
168         try {
169             $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
170         } catch (NotFoundException $e) {
171             $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
172             if ($page === null) {
173                 throw $e;
174             }
175             return redirect($page->getUrl());
176         }
177
178         $this->checkOwnablePermission('page-view', $page);
179
180         $page->html = $this->pageRepo->renderPage($page);
181         $sidebarTree = $this->pageRepo->getBookChildren($page->book);
182         $pageNav = $this->pageRepo->getPageNav($page->html);
183
184         // check if the comment's are enabled
185         $commentsEnabled = !setting('app-disable-comments');
186         if ($commentsEnabled) {
187             $page->load(['comments.createdBy']);
188         }
189
190         Views::add($page);
191         $this->setPageTitle($page->getShortName());
192         return view('pages.show', [
193             'page' => $page,'book' => $page->book,
194             'current' => $page,
195             'sidebarTree' => $sidebarTree,
196             'commentsEnabled' => $commentsEnabled,
197             'pageNav' => $pageNav
198         ]);
199     }
200
201     /**
202      * Get page from an ajax request.
203      * @param int $pageId
204      * @return JsonResponse
205      */
206     public function getPageAjax($pageId)
207     {
208         $page = $this->pageRepo->getById('page', $pageId);
209         return response()->json($page);
210     }
211
212     /**
213      * Show the form for editing the specified page.
214      * @param string $bookSlug
215      * @param string $pageSlug
216      * @return Response
217      * @throws NotFoundException
218      */
219     public function edit($bookSlug, $pageSlug)
220     {
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;
225
226         // Check for active editing
227         $warnings = [];
228         if ($this->pageRepo->isPageEditingActive($page, 60)) {
229             $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
230         }
231
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);
240         }
241
242         if (count($warnings) > 0) {
243             $this->showWarningNotification( implode("\n", $warnings));
244         }
245
246         $draftsEnabled = $this->signedIn;
247         $templates = $this->pageRepo->getPageTemplates(10);
248
249         return view('pages.edit', [
250             'page' => $page,
251             'book' => $page->book,
252             'current' => $page,
253             'draftsEnabled' => $draftsEnabled,
254             'templates' => $templates,
255         ]);
256     }
257
258     /**
259      * Update the specified page in storage.
260      * @param  Request $request
261      * @param  string $bookSlug
262      * @param  string $pageSlug
263      * @return Response
264      */
265     public function update(Request $request, $bookSlug, $pageSlug)
266     {
267         $this->validate($request, [
268             'name' => 'required|string|max:255'
269         ]);
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());
275     }
276
277     /**
278      * Save a draft update as a revision.
279      * @param Request $request
280      * @param int $pageId
281      * @return JsonResponse
282      */
283     public function saveDraft(Request $request, $pageId)
284     {
285         $page = $this->pageRepo->getById('page', $pageId, true);
286         $this->checkOwnablePermission('page-update', $page);
287
288         if (!$this->signedIn) {
289             return response()->json([
290                 'status' => 'error',
291                 'message' => trans('errors.guests_cannot_save_drafts'),
292             ], 500);
293         }
294
295         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
296
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
302         ]);
303     }
304
305     /**
306      * Redirect from a special link url which
307      * uses the page id rather than the name.
308      * @param int $pageId
309      * @return RedirectResponse|Redirector
310      */
311     public function redirectFromLink($pageId)
312     {
313         $page = $this->pageRepo->getById('page', $pageId);
314         return redirect($page->getUrl());
315     }
316
317     /**
318      * Show the deletion page for the specified page.
319      * @param string $bookSlug
320      * @param string $pageSlug
321      * @return View
322      */
323     public function showDelete($bookSlug, $pageSlug)
324     {
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]);
329     }
330
331
332     /**
333      * Show the deletion page for the specified page.
334      * @param string $bookSlug
335      * @param int $pageId
336      * @return View
337      * @throws NotFoundException
338      */
339     public function showDeleteDraft($bookSlug, $pageId)
340     {
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]);
345     }
346
347     /**
348      * Remove the specified page from storage.
349      * @param string $bookSlug
350      * @param string $pageSlug
351      * @return Response
352      * @internal param int $id
353      */
354     public function destroy($bookSlug, $pageSlug)
355     {
356         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
357         $book = $page->book;
358         $this->checkOwnablePermission('page-delete', $page);
359         $this->pageRepo->destroyPage($page);
360
361         Activity::addMessage('page_delete', $book->id, $page->name);
362         $this->showSuccessNotification( trans('entities.pages_delete_success'));
363         return redirect($book->getUrl());
364     }
365
366     /**
367      * Remove the specified draft page from storage.
368      * @param string $bookSlug
369      * @param int $pageId
370      * @return Response
371      * @throws NotFoundException
372      */
373     public function destroyDraft($bookSlug, $pageId)
374     {
375         $page = $this->pageRepo->getById('page', $pageId, true);
376         $book = $page->book;
377         $this->checkOwnablePermission('page-update', $page);
378         $this->showSuccessNotification( trans('entities.pages_delete_draft_success'));
379         $this->pageRepo->destroyPage($page);
380         return redirect($book->getUrl());
381     }
382
383     /**
384      * Shows the last revisions for this page.
385      * @param string $bookSlug
386      * @param string $pageSlug
387      * @return View
388      * @throws NotFoundException
389      */
390     public function showRevisions($bookSlug, $pageSlug)
391     {
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]);
395     }
396
397     /**
398      * Shows a preview of a single revision
399      * @param string $bookSlug
400      * @param string $pageSlug
401      * @param int $revisionId
402      * @return View
403      */
404     public function showRevision($bookSlug, $pageSlug, $revisionId)
405     {
406         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
407         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
408         if ($revision === null) {
409             abort(404);
410         }
411
412         $page->fill($revision->toArray());
413         $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
414
415         return view('pages.revision', [
416             'page' => $page,
417             'book' => $page->book,
418             'diff' => null,
419             'revision' => $revision
420         ]);
421     }
422
423     /**
424      * Shows the changes of a single revision
425      * @param string $bookSlug
426      * @param string $pageSlug
427      * @param int $revisionId
428      * @return View
429      */
430     public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
431     {
432         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
433         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
434         if ($revision === null) {
435             abort(404);
436         }
437
438         $prev = $revision->getPrevious();
439         $prevContent = ($prev === null) ? '' : $prev->html;
440         $diff = (new Htmldiff)->diff($prevContent, $revision->html);
441
442         $page->fill($revision->toArray());
443         $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
444
445         return view('pages.revision', [
446             'page' => $page,
447             'book' => $page->book,
448             'diff' => $diff,
449             'revision' => $revision
450         ]);
451     }
452
453     /**
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
459      */
460     public function restoreRevision($bookSlug, $pageSlug, $revisionId)
461     {
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());
467     }
468
469
470     /**
471      * Deletes a revision using the id of the specified revision.
472      * @param string $bookSlug
473      * @param string $pageSlug
474      * @param int $revId
475      * @return RedirectResponse|Redirector
476      *@throws BadRequestException
477      * @throws NotFoundException
478      */
479     public function destroyRevision($bookSlug, $pageSlug, $revId)
480     {
481         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
482         $this->checkOwnablePermission('page-delete', $page);
483
484         $revision = $page->revisions()->where('id', '=', $revId)->first();
485         if ($revision === null) {
486             throw new NotFoundException("Revision #{$revId} not found");
487         }
488
489         // Get the current revision for the page
490         $currentRevision = $page->getCurrentRevision();
491
492         // Check if its the latest revision, cannot delete latest revision.
493         if (intval($currentRevision->id) === intval($revId)) {
494             $this->showErrorNotification( trans('entities.revision_cannot_delete_latest'));
495             return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
496         }
497
498         $revision->delete();
499         $this->showSuccessNotification( trans('entities.revision_delete_success'));
500         return redirect($page->getUrl('/revisions'));
501     }
502
503     /**
504      * Show a listing of recently created pages
505      * @return Factory|View
506      */
507     public function showRecentlyUpdated()
508     {
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'),
513             'pages' => $pages
514         ]);
515     }
516
517     /**
518      * Show the view to choose a new parent to move a page into.
519      * @param string $bookSlug
520      * @param string $pageSlug
521      * @return mixed
522      * @throws NotFoundException
523      */
524     public function showMove($bookSlug, $pageSlug)
525     {
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,
531             'page' => $page
532         ]);
533     }
534
535     /**
536      * Does the action of moving the location of a page
537      * @param Request $request
538      * @param string $bookSlug
539      * @param string $pageSlug
540      * @return mixed
541      * @throws NotFoundException
542      * @throws Throwable
543      */
544     public function move(Request $request, string $bookSlug, string $pageSlug)
545     {
546         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
547         $this->checkOwnablePermission('page-update', $page);
548         $this->checkOwnablePermission('page-delete', $page);
549
550         $entitySelection = $request->get('entity_selection', null);
551         if ($entitySelection === null || $entitySelection === '') {
552             return redirect($page->getUrl());
553         }
554
555         $stringExploded = explode(':', $entitySelection);
556         $entityType = $stringExploded[0];
557         $entityId = intval($stringExploded[1]);
558
559
560         try {
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();
565         }
566
567         $this->checkOwnablePermission('page-create', $parent);
568
569         $this->pageRepo->changePageParent($page, $parent);
570         Activity::add($page, 'page_move', $page->book->id);
571         $this->showSuccessNotification( trans('entities.pages_move_success', ['parentName' => $parent->name]));
572
573         return redirect($page->getUrl());
574     }
575
576     /**
577      * Show the view to copy a page.
578      * @param string $bookSlug
579      * @param string $pageSlug
580      * @return mixed
581      * @throws NotFoundException
582      */
583     public function showCopy($bookSlug, $pageSlug)
584     {
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,
590             'page' => $page
591         ]);
592     }
593
594     /**
595      * Create a copy of a page within the requested target destination.
596      * @param Request $request
597      * @param string $bookSlug
598      * @param string $pageSlug
599      * @return mixed
600      * @throws NotFoundException
601      * @throws Throwable
602      */
603     public function copy(Request $request, string $bookSlug, string $pageSlug)
604     {
605         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
606         $this->checkOwnablePermission('page-view', $page);
607
608         $entitySelection = $request->get('entity_selection', null);
609         if ($entitySelection === null || $entitySelection === '') {
610             $parent = $page->chapter ? $page->chapter : $page->book;
611         } else {
612             $stringExploded = explode(':', $entitySelection);
613             $entityType = $stringExploded[0];
614             $entityId = intval($stringExploded[1]);
615
616             try {
617                 $parent = $this->pageRepo->getById($entityType, $entityId);
618             } catch (Exception $e) {
619                 $this->showErrorNotification(trans('entities.selected_book_chapter_not_found'));
620                 return redirect()->back();
621             }
622         }
623
624         $this->checkOwnablePermission('page-create', $parent);
625
626         $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
627
628         Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
629         $this->showSuccessNotification( trans('entities.pages_copy_success'));
630
631         return redirect($pageCopy->getUrl());
632     }
633
634     /**
635      * Show the Permissions view.
636      * @param string $bookSlug
637      * @param string $pageSlug
638      * @return Factory|View
639      * @throws NotFoundException
640      */
641     public function showPermissions($bookSlug, $pageSlug)
642     {
643         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
644         $this->checkOwnablePermission('restrictions-manage', $page);
645         $roles = $this->userRepo->getRestrictableRoles();
646         return view('pages.permissions', [
647             'page'  => $page,
648             'roles' => $roles
649         ]);
650     }
651
652     /**
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
659      * @throws Throwable
660      */
661     public function permissions(Request $request, string $bookSlug, string $pageSlug)
662     {
663         $page = $this->pageRepo->getBySlug($pageSlug, $bookSlug);
664         $this->checkOwnablePermission('restrictions-manage', $page);
665         $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
666         $this->showSuccessNotification( trans('entities.pages_permissions_success'));
667         return redirect($page->getUrl());
668     }
669 }