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