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