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