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