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