]> BookStack Code Mirror - bookstack/blob - app/Http/Controllers/PageController.php
Localised draft save time display
[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', $draft);
73         $this->setPageTitle('Edit Page Draft');
74
75         return view('pages/create', ['draft' => $draft, 'book' => $book]);
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 = $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         $sidebarTree = $this->bookRepo->getChildren($book);
132         Views::add($page);
133         $this->setPageTitle($page->getShortName());
134         return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
135     }
136
137     /**
138      * Get page from an ajax request.
139      * @param $pageId
140      * @return \Illuminate\Http\JsonResponse
141      */
142     public function getPageAjax($pageId)
143     {
144         $page = $this->pageRepo->getById($pageId);
145         return response()->json($page);
146     }
147
148     /**
149      * Show the form for editing the specified page.
150      * @param $bookSlug
151      * @param $pageSlug
152      * @return Response
153      */
154     public function edit($bookSlug, $pageSlug)
155     {
156         $book = $this->bookRepo->getBySlug($bookSlug);
157         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
158         $this->checkOwnablePermission('page-update', $page);
159         $this->setPageTitle('Editing Page ' . $page->getShortName());
160         $page->isDraft = false;
161
162         // Check for active editing
163         $warnings = [];
164         if ($this->pageRepo->isPageEditingActive($page, 60)) {
165             $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
166         }
167
168         // Check for a current draft version for this user
169         if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
170             $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
171             $page->name = $draft->name;
172             $page->html = $draft->html;
173             $page->markdown = $draft->markdown;
174             $page->isDraft = true;
175             $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
176         }
177
178         if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
179
180         return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
181     }
182
183     /**
184      * Update the specified page in storage.
185      * @param  Request $request
186      * @param          $bookSlug
187      * @param          $pageSlug
188      * @return Response
189      */
190     public function update(Request $request, $bookSlug, $pageSlug)
191     {
192         $this->validate($request, [
193             'name' => 'required|string|max:255'
194         ]);
195         $book = $this->bookRepo->getBySlug($bookSlug);
196         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
197         $this->checkOwnablePermission('page-update', $page);
198         $this->pageRepo->updatePage($page, $book->id, $request->all());
199         Activity::add($page, 'page_update', $book->id);
200         return redirect($page->getUrl());
201     }
202
203     /**
204      * Save a draft update as a revision.
205      * @param Request $request
206      * @param $pageId
207      * @return \Illuminate\Http\JsonResponse
208      */
209     public function saveDraft(Request $request, $pageId)
210     {
211         $page = $this->pageRepo->getById($pageId, true);
212         $this->checkOwnablePermission('page-update', $page);
213         if ($page->draft) {
214             $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
215         } else {
216             $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
217         }
218
219         $updateTime = $draft->updated_at->timestamp;
220         $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
221         return response()->json([
222             'status' => 'success',
223             'message' => 'Draft saved at ',
224             'timestamp' => $utcUpdateTimestamp
225         ]);
226     }
227
228     /**
229      * Redirect from a special link url which
230      * uses the page id rather than the name.
231      * @param $pageId
232      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
233      */
234     public function redirectFromLink($pageId)
235     {
236         $page = $this->pageRepo->getById($pageId);
237         return redirect($page->getUrl());
238     }
239
240     /**
241      * Show the deletion page for the specified page.
242      * @param $bookSlug
243      * @param $pageSlug
244      * @return \Illuminate\View\View
245      */
246     public function showDelete($bookSlug, $pageSlug)
247     {
248         $book = $this->bookRepo->getBySlug($bookSlug);
249         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
250         $this->checkOwnablePermission('page-delete', $page);
251         $this->setPageTitle('Delete Page ' . $page->getShortName());
252         return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
253     }
254
255
256     /**
257      * Show the deletion page for the specified page.
258      * @param $bookSlug
259      * @param $pageId
260      * @return \Illuminate\View\View
261      * @throws NotFoundException
262      */
263     public function showDeleteDraft($bookSlug, $pageId)
264     {
265         $book = $this->bookRepo->getBySlug($bookSlug);
266         $page = $this->pageRepo->getById($pageId, true);
267         $this->checkOwnablePermission('page-update', $page);
268         $this->setPageTitle('Delete Draft Page ' . $page->getShortName());
269         return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
270     }
271
272     /**
273      * Remove the specified page from storage.
274      * @param $bookSlug
275      * @param $pageSlug
276      * @return Response
277      * @internal param int $id
278      */
279     public function destroy($bookSlug, $pageSlug)
280     {
281         $book = $this->bookRepo->getBySlug($bookSlug);
282         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
283         $this->checkOwnablePermission('page-delete', $page);
284         Activity::addMessage('page_delete', $book->id, $page->name);
285         session()->flash('success', 'Page deleted');
286         $this->pageRepo->destroy($page);
287         return redirect($book->getUrl());
288     }
289
290     /**
291      * Remove the specified draft page from storage.
292      * @param $bookSlug
293      * @param $pageId
294      * @return Response
295      * @throws NotFoundException
296      */
297     public function destroyDraft($bookSlug, $pageId)
298     {
299         $book = $this->bookRepo->getBySlug($bookSlug);
300         $page = $this->pageRepo->getById($pageId, true);
301         $this->checkOwnablePermission('page-update', $page);
302         session()->flash('success', 'Draft deleted');
303         $this->pageRepo->destroy($page);
304         return redirect($book->getUrl());
305     }
306
307     /**
308      * Shows the last revisions for this page.
309      * @param $bookSlug
310      * @param $pageSlug
311      * @return \Illuminate\View\View
312      */
313     public function showRevisions($bookSlug, $pageSlug)
314     {
315         $book = $this->bookRepo->getBySlug($bookSlug);
316         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
317         $this->setPageTitle('Revisions For ' . $page->getShortName());
318         return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
319     }
320
321     /**
322      * Shows a preview of a single revision
323      * @param $bookSlug
324      * @param $pageSlug
325      * @param $revisionId
326      * @return \Illuminate\View\View
327      */
328     public function showRevision($bookSlug, $pageSlug, $revisionId)
329     {
330         $book = $this->bookRepo->getBySlug($bookSlug);
331         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
332         $revision = $this->pageRepo->getRevisionById($revisionId);
333         $page->fill($revision->toArray());
334         $this->setPageTitle('Page Revision For ' . $page->getShortName());
335         return view('pages/revision', ['page' => $page, 'book' => $book]);
336     }
337
338     /**
339      * Restores a page using the content of the specified revision.
340      * @param $bookSlug
341      * @param $pageSlug
342      * @param $revisionId
343      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
344      */
345     public function restoreRevision($bookSlug, $pageSlug, $revisionId)
346     {
347         $book = $this->bookRepo->getBySlug($bookSlug);
348         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
349         $this->checkOwnablePermission('page-update', $page);
350         $page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
351         Activity::add($page, 'page_restore', $book->id);
352         return redirect($page->getUrl());
353     }
354
355     /**
356      * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
357      * https://github.com/barryvdh/laravel-dompdf
358      * @param $bookSlug
359      * @param $pageSlug
360      * @return \Illuminate\Http\Response
361      */
362     public function exportPdf($bookSlug, $pageSlug)
363     {
364         $book = $this->bookRepo->getBySlug($bookSlug);
365         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
366         $pdfContent = $this->exportService->pageToPdf($page);
367         return response()->make($pdfContent, 200, [
368             'Content-Type'        => 'application/octet-stream',
369             'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
370         ]);
371     }
372
373     /**
374      * Export a page to a self-contained HTML file.
375      * @param $bookSlug
376      * @param $pageSlug
377      * @return \Illuminate\Http\Response
378      */
379     public function exportHtml($bookSlug, $pageSlug)
380     {
381         $book = $this->bookRepo->getBySlug($bookSlug);
382         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
383         $containedHtml = $this->exportService->pageToContainedHtml($page);
384         return response()->make($containedHtml, 200, [
385             'Content-Type'        => 'application/octet-stream',
386             'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
387         ]);
388     }
389
390     /**
391      * Export a page to a simple plaintext .txt file.
392      * @param $bookSlug
393      * @param $pageSlug
394      * @return \Illuminate\Http\Response
395      */
396     public function exportPlainText($bookSlug, $pageSlug)
397     {
398         $book = $this->bookRepo->getBySlug($bookSlug);
399         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
400         $containedHtml = $this->exportService->pageToPlainText($page);
401         return response()->make($containedHtml, 200, [
402             'Content-Type'        => 'application/octet-stream',
403             'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
404         ]);
405     }
406
407     /**
408      * Show a listing of recently created pages
409      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
410      */
411     public function showRecentlyCreated()
412     {
413         $pages = $this->pageRepo->getRecentlyCreatedPaginated(20);
414         return view('pages/detailed-listing', [
415             'title' => 'Recently Created Pages',
416             'pages' => $pages
417         ]);
418     }
419
420     /**
421      * Show a listing of recently created pages
422      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
423      */
424     public function showRecentlyUpdated()
425     {
426         $pages = $this->pageRepo->getRecentlyUpdatedPaginated(20);
427         return view('pages/detailed-listing', [
428             'title' => 'Recently Updated Pages',
429             'pages' => $pages
430         ]);
431     }
432
433     /**
434      * Show the Restrictions view.
435      * @param $bookSlug
436      * @param $pageSlug
437      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
438      */
439     public function showRestrict($bookSlug, $pageSlug)
440     {
441         $book = $this->bookRepo->getBySlug($bookSlug);
442         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
443         $this->checkOwnablePermission('restrictions-manage', $page);
444         $roles = $this->userRepo->getRestrictableRoles();
445         return view('pages/restrictions', [
446             'page'  => $page,
447             'roles' => $roles
448         ]);
449     }
450
451     /**
452      * Set the restrictions for this page.
453      * @param $bookSlug
454      * @param $pageSlug
455      * @param Request $request
456      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
457      */
458     public function restrict($bookSlug, $pageSlug, Request $request)
459     {
460         $book = $this->bookRepo->getBySlug($bookSlug);
461         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
462         $this->checkOwnablePermission('restrictions-manage', $page);
463         $this->pageRepo->updateRestrictionsFromRequest($request, $page);
464         session()->flash('success', 'Page Restrictions Updated');
465         return redirect($page->getUrl());
466     }
467
468 }